From 40751fecb7fb2f813887a2135bbe217ed4ceaf07 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 28 Mar 2026 18:28:16 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20HA=20light=20target=20live=20color=20pr?= =?UTF-8?q?eview=20=E2=80=94=20per-entity=20swatches=20via=20WebSocket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cache per-entity colors in HALightTargetProcessor._update_lights() - Broadcast colors_update to WS clients at target's update_rate - WS endpoint: /api/v1/output-targets/{target_id}/ha-light/ws - Frontend: connect WS when target runs, update swatch colors live - Card shows colored boxes per mapped entity with entity name labels --- .../api/routes/output_targets_control.py | 97 +- .../api/routes/output_targets_keycolors.py | 528 --- .../api/routes/pattern_templates.py | 33 +- .../api/schemas/output_targets.py | 85 +- .../processing/ha_light_target_processor.py | 47 +- .../core/processing/kc_target_processor.py | 489 -- .../core/processing/processor_manager.py | 11 +- .../core/processing/target_processor.py | 2 +- server/src/wled_controller/main.py | 1 - .../src/wled_controller/static/css/modal.css | 29 + server/src/wled_controller/static/js/app.ts | 2 +- .../static/js/core/command-palette.ts | 19 +- .../static/js/core/graph-nodes.ts | 4 +- .../wled_controller/static/js/core/icons.ts | 2 +- .../wled_controller/static/js/core/state.ts | 19 - .../src/wled_controller/static/js/core/ui.ts | 16 +- .../static/js/features/graph-editor.ts | 2 +- .../static/js/features/ha-light-targets.ts | 75 + .../static/js/features/kc-targets.ts | 841 ---- .../static/js/features/targets.ts | 32 +- .../src/wled_controller/static/js/global.d.ts | 16 +- server/src/wled_controller/static/js/types.ts | 27 +- .../wled_controller/static/locales/en.json | 4173 ++++++++--------- .../wled_controller/static/locales/ru.json | 3867 ++++++++------- .../wled_controller/static/locales/zh.json | 3863 ++++++++------- .../storage/key_colors_output_target.py | 137 - .../wled_controller/storage/output_target.py | 10 +- .../storage/pattern_template.py | 46 +- .../storage/pattern_template_store.py | 12 +- .../templates/modals/kc-editor.html | 92 - .../tests/storage/test_output_target_store.py | 19 +- 31 files changed, 6245 insertions(+), 8351 deletions(-) delete mode 100644 server/src/wled_controller/api/routes/output_targets_keycolors.py delete mode 100644 server/src/wled_controller/core/processing/kc_target_processor.py delete mode 100644 server/src/wled_controller/static/js/features/kc-targets.ts delete mode 100644 server/src/wled_controller/storage/key_colors_output_target.py delete mode 100644 server/src/wled_controller/templates/modals/kc-editor.html diff --git a/server/src/wled_controller/api/routes/output_targets_control.py b/server/src/wled_controller/api/routes/output_targets_control.py index e01dd32..14468eb 100644 --- a/server/src/wled_controller/api/routes/output_targets_control.py +++ b/server/src/wled_controller/api/routes/output_targets_control.py @@ -3,7 +3,6 @@ Extracted from output_targets.py to keep files under 800 lines. """ - from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect from wled_controller.api.auth import AuthRequired @@ -22,7 +21,10 @@ from wled_controller.api.schemas.output_targets import ( from wled_controller.core.processing.processor_manager import ProcessorManager from wled_controller.core.capture.screen_capture import get_available_displays from wled_controller.storage.color_strip_store import ColorStripStore -from wled_controller.storage.color_strip_source import AdvancedPictureColorStripSource, PictureColorStripSource +from wled_controller.storage.color_strip_source import ( + AdvancedPictureColorStripSource, + PictureColorStripSource, +) from wled_controller.storage.picture_source_store import PictureSourceStore from wled_controller.storage.wled_output_target import WledOutputTarget from wled_controller.storage.output_target_store import OutputTargetStore @@ -35,7 +37,10 @@ router = APIRouter() # ===== BULK PROCESSING CONTROL ENDPOINTS ===== -@router.post("/api/v1/output-targets/bulk/start", response_model=BulkTargetResponse, tags=["Processing"]) + +@router.post( + "/api/v1/output-targets/bulk/start", response_model=BulkTargetResponse, tags=["Processing"] +) async def bulk_start_processing( body: BulkTargetRequest, _auth: AuthRequired, @@ -67,7 +72,9 @@ async def bulk_start_processing( return BulkTargetResponse(started=started, errors=errors) -@router.post("/api/v1/output-targets/bulk/stop", response_model=BulkTargetResponse, tags=["Processing"]) +@router.post( + "/api/v1/output-targets/bulk/stop", response_model=BulkTargetResponse, tags=["Processing"] +) async def bulk_stop_processing( body: BulkTargetRequest, _auth: AuthRequired, @@ -93,6 +100,7 @@ async def bulk_stop_processing( # ===== PROCESSING CONTROL ENDPOINTS ===== + @router.post("/api/v1/output-targets/{target_id}/start", tags=["Processing"]) async def start_processing( target_id: str, @@ -146,7 +154,12 @@ async def stop_processing( # ===== STATE & METRICS ENDPOINTS ===== -@router.get("/api/v1/output-targets/{target_id}/state", response_model=TargetProcessingState, tags=["Processing"]) + +@router.get( + "/api/v1/output-targets/{target_id}/state", + response_model=TargetProcessingState, + tags=["Processing"], +) async def get_target_state( target_id: str, _auth: AuthRequired, @@ -164,7 +177,11 @@ async def get_target_state( raise HTTPException(status_code=500, detail="Internal server error") -@router.get("/api/v1/output-targets/{target_id}/metrics", response_model=TargetMetricsResponse, tags=["Metrics"]) +@router.get( + "/api/v1/output-targets/{target_id}/metrics", + response_model=TargetMetricsResponse, + tags=["Metrics"], +) async def get_target_metrics( target_id: str, _auth: AuthRequired, @@ -192,6 +209,7 @@ async def events_ws( ): """WebSocket for real-time state change events. Auth via ?token=.""" from wled_controller.api.auth import verify_ws_token + if not verify_ws_token(token): await websocket.close(code=4001, reason="Unauthorized") return @@ -215,6 +233,7 @@ async def events_ws( # ===== OVERLAY VISUALIZATION ===== + @router.post("/api/v1/output-targets/{target_id}/overlay/start", tags=["Visualization"]) async def start_target_overlay( target_id: str, @@ -247,10 +266,16 @@ async def start_target_overlay( if first_css_id: try: css = color_strip_store.get_source(first_css_id) - if isinstance(css, (PictureColorStripSource, AdvancedPictureColorStripSource)) and css.calibration: + if ( + isinstance(css, (PictureColorStripSource, AdvancedPictureColorStripSource)) + and css.calibration + ): calibration = css.calibration # Resolve the display this CSS is capturing - from wled_controller.api.routes.color_strip_sources import _resolve_display_index + from wled_controller.api.routes.color_strip_sources import ( + _resolve_display_index, + ) + ps_id = getattr(css, "picture_source_id", "") or "" display_index = _resolve_display_index(ps_id, picture_source_store) displays = get_available_displays() @@ -258,9 +283,13 @@ async def start_target_overlay( display_index = min(display_index, len(displays) - 1) display_info = displays[display_index] except Exception as e: - logger.warning(f"Could not pre-load CSS calibration for overlay on {target_id}: {e}") + logger.warning( + f"Could not pre-load CSS calibration for overlay on {target_id}: {e}" + ) - await manager.start_overlay(target_id, target.name, calibration=calibration, display_info=display_info) + await manager.start_overlay( + target_id, target.name, calibration=calibration, display_info=display_info + ) return {"status": "started", "target_id": target_id} except ValueError as e: @@ -305,8 +334,55 @@ async def get_overlay_status( raise HTTPException(status_code=404, detail=str(e)) +# ===== HA LIGHT COLOR PREVIEW WEBSOCKET ===== + + +@router.websocket("/api/v1/output-targets/{target_id}/ha-light/ws") +async def ha_light_colors_ws( + websocket: WebSocket, + target_id: str, + token: str = Query(""), +): + """WebSocket for live HA light entity color preview. + + Streams: {"type": "colors_update", "colors": {entity_id: {r,g,b,hex}, ...}} + at the target's update_rate. + """ + from wled_controller.api.auth import verify_ws_token + + if not verify_ws_token(token): + await websocket.close(code=4001, reason="Unauthorized") + return + + manager: ProcessorManager = get_processor_manager() + + try: + proc = manager._processors.get(target_id) + if not proc or not proc.is_running: + await websocket.close(code=4003, reason="Target not running") + return + except Exception as e: + await websocket.close(code=4004, reason=str(e)) + return + + await websocket.accept() + + try: + manager.add_ha_light_ws_client(target_id, websocket) + while True: + # Keep connection alive — wait for client disconnect + await websocket.receive_text() + except WebSocketDisconnect: + pass + except Exception: + pass + finally: + manager.remove_ha_light_ws_client(target_id, websocket) + + # ===== LED PREVIEW WEBSOCKET ===== + @router.websocket("/api/v1/output-targets/{target_id}/led-preview/ws") async def led_preview_ws( websocket: WebSocket, @@ -315,6 +391,7 @@ async def led_preview_ws( ): """WebSocket for real-time LED strip preview. Sends binary RGB frames. Auth via ?token=.""" from wled_controller.api.auth import verify_ws_token + if not verify_ws_token(token): await websocket.close(code=4001, reason="Unauthorized") return diff --git a/server/src/wled_controller/api/routes/output_targets_keycolors.py b/server/src/wled_controller/api/routes/output_targets_keycolors.py deleted file mode 100644 index a752833..0000000 --- a/server/src/wled_controller/api/routes/output_targets_keycolors.py +++ /dev/null @@ -1,528 +0,0 @@ -"""Output target routes: key colors endpoints, testing, and WebSocket streams. - -Extracted from output_targets.py to keep files under 800 lines. -""" - -import asyncio -import time - -import numpy as np -from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect - -from wled_controller.api.auth import AuthRequired -from wled_controller.api.dependencies import ( - get_device_store, - get_output_target_store, - get_pattern_template_store, - get_picture_source_store, - get_pp_template_store, - get_processor_manager, - get_template_store, -) -from wled_controller.api.schemas.output_targets import ( - ExtractedColorResponse, - KCTestRectangleResponse, - KCTestResponse, - KeyColorsResponse, -) -from wled_controller.core.capture_engines import EngineRegistry -from wled_controller.core.filters import FilterRegistry, ImagePool -from wled_controller.core.processing.processor_manager import ProcessorManager -from wled_controller.core.capture.screen_capture import ( - calculate_average_color, - calculate_dominant_color, - calculate_median_color, -) -from wled_controller.storage import DeviceStore -from wled_controller.storage.pattern_template_store import PatternTemplateStore -from wled_controller.storage.picture_source import ScreenCapturePictureSource, StaticImagePictureSource -from wled_controller.storage.picture_source_store import PictureSourceStore -from wled_controller.storage.template_store import TemplateStore -from wled_controller.storage.key_colors_output_target import KeyColorsOutputTarget -from wled_controller.storage.output_target_store import OutputTargetStore -from wled_controller.storage.base_store import EntityNotFoundError -from wled_controller.utils import get_logger - -logger = get_logger(__name__) - -router = APIRouter() - - -# ===== KEY COLORS ENDPOINTS ===== - -@router.get("/api/v1/output-targets/{target_id}/colors", response_model=KeyColorsResponse, tags=["Key Colors"]) -async def get_target_colors( - target_id: str, - _auth: AuthRequired, - manager: ProcessorManager = Depends(get_processor_manager), -): - """Get latest extracted colors for a key-colors target (polling).""" - try: - raw_colors = manager.get_kc_latest_colors(target_id) - colors = {} - for name, (r, g, b) in raw_colors.items(): - colors[name] = ExtractedColorResponse( - r=r, g=g, b=b, - hex=f"#{r:02x}{g:02x}{b:02x}", - ) - from datetime import datetime, timezone - return KeyColorsResponse( - target_id=target_id, - colors=colors, - timestamp=datetime.now(timezone.utc), - ) - except ValueError as e: - raise HTTPException(status_code=404, detail=str(e)) - - -@router.post("/api/v1/output-targets/{target_id}/test", response_model=KCTestResponse, tags=["Key Colors"]) -async def test_kc_target( - target_id: str, - _auth: AuthRequired, - target_store: OutputTargetStore = Depends(get_output_target_store), - source_store: PictureSourceStore = Depends(get_picture_source_store), - template_store: TemplateStore = Depends(get_template_store), - pattern_store: PatternTemplateStore = Depends(get_pattern_template_store), - processor_manager: ProcessorManager = Depends(get_processor_manager), - device_store: DeviceStore = Depends(get_device_store), - pp_template_store=Depends(get_pp_template_store), -): - """Test a key-colors target: capture a frame, extract colors from each rectangle.""" - - stream = None - try: - # 1. Load and validate KC target - try: - target = target_store.get_target(target_id) - except ValueError as e: - raise HTTPException(status_code=404, detail=str(e)) - - if not isinstance(target, KeyColorsOutputTarget): - raise HTTPException(status_code=400, detail="Target is not a key_colors target") - - settings = target.settings - - # 2. Resolve pattern template - if not settings.pattern_template_id: - raise HTTPException(status_code=400, detail="No pattern template configured") - - try: - pattern_tmpl = pattern_store.get_template(settings.pattern_template_id) - except ValueError: - raise HTTPException(status_code=400, detail=f"Pattern template not found: {settings.pattern_template_id}") - - rectangles = pattern_tmpl.rectangles - if not rectangles: - raise HTTPException(status_code=400, detail="Pattern template has no rectangles") - - # 3. Resolve picture source and capture a frame - if not target.picture_source_id: - raise HTTPException(status_code=400, detail="No picture source configured") - - try: - chain = source_store.resolve_stream_chain(target.picture_source_id) - except EntityNotFoundError as e: - raise HTTPException(status_code=404, detail=str(e)) - - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - - raw_stream = chain["raw_stream"] - - from wled_controller.utils.image_codec import load_image_file - - if isinstance(raw_stream, StaticImagePictureSource): - from wled_controller.api.dependencies import get_asset_store as _get_asset_store - - asset_store = _get_asset_store() - image_path = asset_store.get_file_path(raw_stream.image_asset_id) if raw_stream.image_asset_id else None - if not image_path: - raise HTTPException(status_code=400, detail="Image asset not found or missing file") - image = load_image_file(image_path) - - elif isinstance(raw_stream, ScreenCapturePictureSource): - try: - capture_template = template_store.get_template(raw_stream.capture_template_id) - except ValueError: - raise HTTPException( - status_code=400, - detail=f"Capture template not found: {raw_stream.capture_template_id}", - ) - - display_index = raw_stream.display_index - - if capture_template.engine_type not in EngineRegistry.get_available_engines(): - raise HTTPException( - status_code=400, - detail=f"Engine '{capture_template.engine_type}' is not available on this system", - ) - - locked_device_id = processor_manager.get_display_lock_info(display_index) - if locked_device_id: - try: - device = device_store.get_device(locked_device_id) - device_name = device.name - except Exception: - device_name = locked_device_id - raise HTTPException( - status_code=409, - detail=f"Display {display_index} is currently being captured by device '{device_name}'. " - f"Please stop the device processing before testing.", - ) - - stream = EngineRegistry.create_stream( - capture_template.engine_type, display_index, capture_template.engine_config - ) - stream.initialize() - - screen_capture = stream.capture_frame() - if screen_capture is None: - raise RuntimeError("No frame captured") - - if not isinstance(screen_capture.image, np.ndarray): - raise ValueError("Unexpected image format from engine") - image = screen_capture.image - else: - raise HTTPException(status_code=400, detail="Unsupported picture source type") - - # 3b. Apply postprocessing filters (if the picture source has a filter chain) - pp_template_ids = chain.get("postprocessing_template_ids", []) - if pp_template_ids and pp_template_store: - image_pool = ImagePool() - for pp_id in pp_template_ids: - try: - pp_template = pp_template_store.get_template(pp_id) - except ValueError: - logger.warning(f"KC test: PP template {pp_id} not found, skipping") - continue - flat_filters = pp_template_store.resolve_filter_instances(pp_template.filters) - for fi in flat_filters: - try: - f = FilterRegistry.create_instance(fi.filter_id, fi.options) - result = f.process_image(image, image_pool) - if result is not None: - image = result - except ValueError: - logger.warning(f"KC test: unknown filter '{fi.filter_id}', skipping") - - # 4. Extract colors from each rectangle - img_array = image - h, w = img_array.shape[:2] - - calc_fns = { - "average": calculate_average_color, - "median": calculate_median_color, - "dominant": calculate_dominant_color, - } - calc_fn = calc_fns.get(settings.interpolation_mode, calculate_average_color) - - result_rects = [] - for rect in rectangles: - px_x = max(0, int(rect.x * w)) - px_y = max(0, int(rect.y * h)) - px_w = max(1, int(rect.width * w)) - px_h = max(1, int(rect.height * h)) - px_x = min(px_x, w - 1) - px_y = min(px_y, h - 1) - px_w = min(px_w, w - px_x) - px_h = min(px_h, h - px_y) - - sub_img = img_array[px_y:px_y + px_h, px_x:px_x + px_w] - r, g, b = calc_fn(sub_img) - - result_rects.append(KCTestRectangleResponse( - name=rect.name, - x=rect.x, - y=rect.y, - width=rect.width, - height=rect.height, - color=ExtractedColorResponse(r=r, g=g, b=b, hex=f"#{r:02x}{g:02x}{b:02x}"), - )) - - # 5. Encode frame as base64 JPEG - from wled_controller.utils.image_codec import encode_jpeg_data_uri - image_data_uri = encode_jpeg_data_uri(image, quality=90) - - return KCTestResponse( - image=image_data_uri, - rectangles=result_rects, - interpolation_mode=settings.interpolation_mode, - pattern_template_name=pattern_tmpl.name, - ) - - except HTTPException: - raise - except EntityNotFoundError as e: - raise HTTPException(status_code=404, detail=str(e)) - - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - except RuntimeError as e: - logger.error("Capture error during KC target test: %s", e, exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - except Exception as e: - logger.error("Failed to test KC target: %s", e, exc_info=True) - raise HTTPException(status_code=500, detail="Internal server error") - finally: - if stream: - try: - stream.cleanup() - except Exception as e: - logger.error(f"Error cleaning up test stream: {e}") - - -@router.websocket("/api/v1/output-targets/{target_id}/test/ws") -async def test_kc_target_ws( - websocket: WebSocket, - target_id: str, - token: str = Query(""), - fps: int = Query(3), - preview_width: int = Query(480), -): - """WebSocket for real-time KC target test preview. Auth via ?token=. - - Streams JSON frames: {"type": "frame", "image": "data:image/jpeg;base64,...", - "rectangles": [...], "pattern_template_name": "...", "interpolation_mode": "..."} - """ - import json as _json - from wled_controller.api.auth import verify_ws_token - - if not verify_ws_token(token): - await websocket.close(code=4001, reason="Unauthorized") - return - - # Load stores - target_store_inst: OutputTargetStore = get_output_target_store() - source_store_inst: PictureSourceStore = get_picture_source_store() - get_template_store() - pattern_store_inst: PatternTemplateStore = get_pattern_template_store() - processor_manager_inst: ProcessorManager = get_processor_manager() - device_store_inst: DeviceStore = get_device_store() - pp_template_store_inst = get_pp_template_store() - - # Validate target - try: - target = target_store_inst.get_target(target_id) - except ValueError as e: - await websocket.close(code=4004, reason=str(e)) - return - - if not isinstance(target, KeyColorsOutputTarget): - await websocket.close(code=4003, reason="Target is not a key_colors target") - return - - settings = target.settings - - if not settings.pattern_template_id: - await websocket.close(code=4003, reason="No pattern template configured") - return - - try: - pattern_tmpl = pattern_store_inst.get_template(settings.pattern_template_id) - except ValueError: - await websocket.close(code=4003, reason=f"Pattern template not found: {settings.pattern_template_id}") - return - - rectangles = pattern_tmpl.rectangles - if not rectangles: - await websocket.close(code=4003, reason="Pattern template has no rectangles") - return - - if not target.picture_source_id: - await websocket.close(code=4003, reason="No picture source configured") - return - - try: - chain = source_store_inst.resolve_stream_chain(target.picture_source_id) - except ValueError as e: - await websocket.close(code=4003, reason=str(e)) - return - - raw_stream = chain["raw_stream"] - - # For screen capture sources, check display lock - if isinstance(raw_stream, ScreenCapturePictureSource): - display_index = raw_stream.display_index - locked_device_id = processor_manager_inst.get_display_lock_info(display_index) - if locked_device_id: - try: - device = device_store_inst.get_device(locked_device_id) - device_name = device.name - except Exception: - device_name = locked_device_id - await websocket.close( - code=4003, - reason=f"Display {display_index} is captured by '{device_name}'. Stop processing first.", - ) - return - - fps = max(1, min(30, fps)) - preview_width = max(120, min(1920, preview_width)) - frame_interval = 1.0 / fps - - calc_fns = { - "average": calculate_average_color, - "median": calculate_median_color, - "dominant": calculate_dominant_color, - } - calc_fn = calc_fns.get(settings.interpolation_mode, calculate_average_color) - - await websocket.accept() - logger.info(f"KC test WS connected for {target_id} (fps={fps})") - - # Use the shared LiveStreamManager so we share the capture stream with - # running LED targets instead of creating a competing DXGI duplicator. - live_stream_mgr = processor_manager_inst._live_stream_manager - live_stream = None - - try: - live_stream = await asyncio.to_thread( - live_stream_mgr.acquire, target.picture_source_id - ) - logger.info(f"KC test WS acquired shared live stream for {target.picture_source_id}") - - prev_frame_ref = None - - while True: - loop_start = time.monotonic() - - try: - capture = await asyncio.to_thread(live_stream.get_latest_frame) - - if capture is None or capture.image is None: - await asyncio.sleep(frame_interval) - continue - - # Skip if same frame object (no new capture yet) - if capture is prev_frame_ref: - await asyncio.sleep(frame_interval * 0.5) - continue - prev_frame_ref = capture - - if not isinstance(capture.image, np.ndarray): - await asyncio.sleep(frame_interval) - continue - cur_image = capture.image - if cur_image is None: - await asyncio.sleep(frame_interval) - continue - - # Apply postprocessing (if the source chain has PP templates) - chain = source_store_inst.resolve_stream_chain(target.picture_source_id) - pp_template_ids = chain.get("postprocessing_template_ids", []) - if pp_template_ids and pp_template_store_inst: - image_pool = ImagePool() - for pp_id in pp_template_ids: - try: - pp_template = pp_template_store_inst.get_template(pp_id) - except ValueError as e: - logger.debug("PP template %s not found during KC test: %s", pp_id, e) - continue - flat_filters = pp_template_store_inst.resolve_filter_instances(pp_template.filters) - for fi in flat_filters: - try: - f = FilterRegistry.create_instance(fi.filter_id, fi.options) - result = f.process_image(cur_image, image_pool) - if result is not None: - cur_image = result - except ValueError as e: - logger.debug("Filter processing error during KC test: %s", e) - pass - - # Extract colors - img_array = cur_image - h, w = img_array.shape[:2] - - result_rects = [] - for rect in rectangles: - px_x = max(0, int(rect.x * w)) - px_y = max(0, int(rect.y * h)) - px_w = max(1, int(rect.width * w)) - px_h = max(1, int(rect.height * h)) - px_x = min(px_x, w - 1) - px_y = min(px_y, h - 1) - px_w = min(px_w, w - px_x) - px_h = min(px_h, h - px_y) - - sub_img = img_array[px_y:px_y + px_h, px_x:px_x + px_w] - r, g, b = calc_fn(sub_img) - - result_rects.append({ - "name": rect.name, - "x": rect.x, - "y": rect.y, - "width": rect.width, - "height": rect.height, - "color": {"r": r, "g": g, "b": b, "hex": f"#{r:02x}{g:02x}{b:02x}"}, - }) - - # Encode frame as JPEG - from wled_controller.utils.image_codec import encode_jpeg_data_uri, resize_down - frame_to_encode = resize_down(cur_image, preview_width) if preview_width else cur_image - frame_uri = encode_jpeg_data_uri(frame_to_encode, quality=85) - - await websocket.send_text(_json.dumps({ - "type": "frame", - "image": frame_uri, - "rectangles": result_rects, - "pattern_template_name": pattern_tmpl.name, - "interpolation_mode": settings.interpolation_mode, - })) - - except (WebSocketDisconnect, Exception) as inner_e: - if isinstance(inner_e, WebSocketDisconnect): - raise - logger.warning(f"KC test WS frame error for {target_id}: {inner_e}") - - elapsed = time.monotonic() - loop_start - sleep_time = frame_interval - elapsed - if sleep_time > 0: - await asyncio.sleep(sleep_time) - - except WebSocketDisconnect: - logger.info(f"KC test WS disconnected for {target_id}") - except Exception as e: - logger.error(f"KC test WS error for {target_id}: {e}", exc_info=True) - finally: - if live_stream is not None: - try: - await asyncio.to_thread( - live_stream_mgr.release, target.picture_source_id - ) - except Exception as e: - logger.debug("Live stream release during KC test cleanup: %s", e) - pass - logger.info(f"KC test WS closed for {target_id}") - - -@router.websocket("/api/v1/output-targets/{target_id}/ws") -async def target_colors_ws( - websocket: WebSocket, - target_id: str, - token: str = Query(""), -): - """WebSocket for real-time key color updates. Auth via ?token=.""" - from wled_controller.api.auth import verify_ws_token - if not verify_ws_token(token): - await websocket.close(code=4001, reason="Unauthorized") - return - - await websocket.accept() - - manager = get_processor_manager() - - try: - manager.add_kc_ws_client(target_id, websocket) - except ValueError: - await websocket.close(code=4004, reason="Target not found") - return - - try: - while True: - # Keep alive — wait for client messages (or disconnect) - await websocket.receive_text() - except WebSocketDisconnect: - logger.debug("KC live WebSocket disconnected for target %s", target_id) - pass - finally: - manager.remove_kc_ws_client(target_id, websocket) diff --git a/server/src/wled_controller/api/routes/pattern_templates.py b/server/src/wled_controller/api/routes/pattern_templates.py index 82f8647..7e31d2a 100644 --- a/server/src/wled_controller/api/routes/pattern_templates.py +++ b/server/src/wled_controller/api/routes/pattern_templates.py @@ -15,7 +15,7 @@ from wled_controller.api.schemas.pattern_templates import ( PatternTemplateUpdate, ) from wled_controller.api.schemas.output_targets import KeyColorRectangleSchema -from wled_controller.storage.key_colors_output_target import KeyColorRectangle +from wled_controller.storage.pattern_template import KeyColorRectangle from wled_controller.storage.pattern_template_store import PatternTemplateStore from wled_controller.storage.output_target_store import OutputTargetStore from wled_controller.utils import get_logger @@ -42,7 +42,11 @@ def _pat_template_to_response(t) -> PatternTemplateResponse: ) -@router.get("/api/v1/pattern-templates", response_model=PatternTemplateListResponse, tags=["Pattern Templates"]) +@router.get( + "/api/v1/pattern-templates", + response_model=PatternTemplateListResponse, + tags=["Pattern Templates"], +) async def list_pattern_templates( _auth: AuthRequired, store: PatternTemplateStore = Depends(get_pattern_template_store), @@ -57,7 +61,12 @@ async def list_pattern_templates( raise HTTPException(status_code=500, detail="Internal server error") -@router.post("/api/v1/pattern-templates", response_model=PatternTemplateResponse, tags=["Pattern Templates"], status_code=201) +@router.post( + "/api/v1/pattern-templates", + response_model=PatternTemplateResponse, + tags=["Pattern Templates"], + status_code=201, +) async def create_pattern_template( data: PatternTemplateCreate, _auth: AuthRequired, @@ -87,7 +96,11 @@ async def create_pattern_template( raise HTTPException(status_code=500, detail="Internal server error") -@router.get("/api/v1/pattern-templates/{template_id}", response_model=PatternTemplateResponse, tags=["Pattern Templates"]) +@router.get( + "/api/v1/pattern-templates/{template_id}", + response_model=PatternTemplateResponse, + tags=["Pattern Templates"], +) async def get_pattern_template( template_id: str, _auth: AuthRequired, @@ -101,7 +114,11 @@ async def get_pattern_template( raise HTTPException(status_code=404, detail=f"Pattern template {template_id} not found") -@router.put("/api/v1/pattern-templates/{template_id}", response_model=PatternTemplateResponse, tags=["Pattern Templates"]) +@router.put( + "/api/v1/pattern-templates/{template_id}", + response_model=PatternTemplateResponse, + tags=["Pattern Templates"], +) async def update_pattern_template( template_id: str, data: PatternTemplateUpdate, @@ -135,7 +152,9 @@ async def update_pattern_template( raise HTTPException(status_code=500, detail="Internal server error") -@router.delete("/api/v1/pattern-templates/{template_id}", status_code=204, tags=["Pattern Templates"]) +@router.delete( + "/api/v1/pattern-templates/{template_id}", status_code=204, tags=["Pattern Templates"] +) async def delete_pattern_template( template_id: str, _auth: AuthRequired, @@ -150,7 +169,7 @@ async def delete_pattern_template( raise HTTPException( status_code=409, detail=f"Cannot delete pattern template: it is referenced by target(s): {names}. " - "Please reassign those targets before deleting.", + "Please reassign those targets before deleting.", ) store.delete_template(template_id) fire_entity_event("pattern_template", "deleted", template_id) diff --git a/server/src/wled_controller/api/schemas/output_targets.py b/server/src/wled_controller/api/schemas/output_targets.py index 76a01ec..fb79726 100644 --- a/server/src/wled_controller/api/schemas/output_targets.py +++ b/server/src/wled_controller/api/schemas/output_targets.py @@ -18,42 +18,6 @@ class KeyColorRectangleSchema(BaseModel): height: float = Field(default=1.0, description="Height (0.0-1.0)", gt=0.0, le=1.0) -class KeyColorsSettingsSchema(BaseModel): - """Settings for key colors extraction.""" - - fps: int = Field(default=10, description="Extraction rate (1-60)", ge=1, le=60) - interpolation_mode: str = Field( - default="average", description="Color mode (average, median, dominant)" - ) - smoothing: float = Field( - default=0.3, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0 - ) - pattern_template_id: str = Field( - default="", description="Pattern template ID for rectangle layout" - ) - brightness: float = Field( - default=1.0, description="Output brightness (0.0-1.0)", ge=0.0, le=1.0 - ) - brightness_value_source_id: str = Field(default="", description="Brightness value source ID") - - -class ExtractedColorResponse(BaseModel): - """A single extracted color.""" - - r: int = Field(description="Red (0-255)") - g: int = Field(description="Green (0-255)") - b: int = Field(description="Blue (0-255)") - hex: str = Field(description="Hex color (#rrggbb)") - - -class KeyColorsResponse(BaseModel): - """Extracted key colors for a target.""" - - target_id: str = Field(description="Target ID") - colors: Dict[str, ExtractedColorResponse] = Field(description="Rectangle name -> color") - timestamp: Optional[datetime] = Field(None, description="Extraction timestamp") - - class HALightMappingSchema(BaseModel): """Maps an LED range to one HA light entity.""" @@ -69,7 +33,7 @@ class OutputTargetCreate(BaseModel): """Request to create an output target.""" name: str = Field(description="Target name", min_length=1, max_length=100) - target_type: str = Field(default="led", description="Target type (led, key_colors, ha_light)") + target_type: str = Field(default="led", description="Target type (led, ha_light)") # LED target fields device_id: str = Field(default="", description="LED device ID") color_strip_source_id: str = Field(default="", description="Color strip source ID") @@ -101,13 +65,6 @@ class OutputTargetCreate(BaseModel): pattern="^(ddp|http)$", description="Send protocol: ddp (UDP) or http (JSON API)", ) - # KC target fields - picture_source_id: str = Field( - default="", description="Picture source ID (for key_colors targets)" - ) - key_colors_settings: Optional[KeyColorsSettingsSchema] = Field( - None, description="Key colors settings (for key_colors targets)" - ) # HA light target fields ha_source_id: str = Field( default="", description="Home Assistant source ID (for ha_light targets)" @@ -157,13 +114,6 @@ class OutputTargetUpdate(BaseModel): protocol: Optional[str] = Field( None, pattern="^(ddp|http)$", description="Send protocol: ddp (UDP) or http (JSON API)" ) - # KC target fields - picture_source_id: Optional[str] = Field( - None, description="Picture source ID (for key_colors targets)" - ) - key_colors_settings: Optional[KeyColorsSettingsSchema] = Field( - None, description="Key colors settings (for key_colors targets)" - ) # HA light target fields ha_source_id: Optional[str] = Field( None, description="Home Assistant source ID (for ha_light targets)" @@ -206,11 +156,6 @@ class OutputTargetResponse(BaseModel): default=False, description="Auto-reduce FPS when device is unresponsive" ) protocol: str = Field(default="ddp", description="Send protocol (ddp or http)") - # KC target fields - picture_source_id: str = Field(default="", description="Picture source ID (key_colors)") - key_colors_settings: Optional[KeyColorsSettingsSchema] = Field( - None, description="Key colors settings" - ) # HA light target fields ha_source_id: str = Field(default="", description="Home Assistant source ID (ha_light)") ha_light_mappings: Optional[List[HALightMappingSchema]] = Field( @@ -263,12 +208,6 @@ class TargetProcessingState(BaseModel): timing_audio_render_ms: Optional[float] = Field( None, description="Audio visualization render time (ms)" ) - timing_calc_colors_ms: Optional[float] = Field( - None, description="Color calculation time (ms, KC targets)" - ) - timing_broadcast_ms: Optional[float] = Field( - None, description="WebSocket broadcast time (ms, KC targets)" - ) display_index: Optional[int] = Field(None, description="Current display index") overlay_active: bool = Field( default=False, description="Whether visualization overlay is active" @@ -328,25 +267,3 @@ class BulkTargetResponse(BaseModel): errors: Dict[str, str] = Field( default_factory=dict, description="Map of target ID to error message for failures" ) - - -class KCTestRectangleResponse(BaseModel): - """A rectangle with its extracted color from a KC test.""" - - name: str = Field(description="Rectangle name") - x: float = Field(description="Left edge (0.0-1.0)") - y: float = Field(description="Top edge (0.0-1.0)") - width: float = Field(description="Width (0.0-1.0)") - height: float = Field(description="Height (0.0-1.0)") - color: ExtractedColorResponse = Field(description="Extracted color for this rectangle") - - -class KCTestResponse(BaseModel): - """Response from testing a KC target.""" - - image: str = Field(description="Base64 data URI of the captured frame") - rectangles: List[KCTestRectangleResponse] = Field( - description="Rectangles with extracted colors" - ) - interpolation_mode: str = Field(description="Color extraction mode used") - pattern_template_name: str = Field(description="Pattern template name") diff --git a/server/src/wled_controller/core/processing/ha_light_target_processor.py b/server/src/wled_controller/core/processing/ha_light_target_processor.py index 12edb98..b6d5541 100644 --- a/server/src/wled_controller/core/processing/ha_light_target_processor.py +++ b/server/src/wled_controller/core/processing/ha_light_target_processor.py @@ -6,8 +6,9 @@ Rate-limited to update_rate Hz (typically 1-5 Hz). """ import asyncio +import json import time -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import numpy as np @@ -50,6 +51,8 @@ class HALightTargetProcessor(TargetProcessor): self._value_stream = None # brightness value source stream self._previous_colors: Dict[str, Tuple[int, int, int]] = {} self._previous_on: Dict[str, bool] = {} # track on/off state per entity + self._latest_entity_colors: Dict[str, Tuple[int, int, int]] = {} + self._ws_clients: List[Any] = [] self._start_time: Optional[float] = None @property @@ -130,6 +133,8 @@ class HALightTargetProcessor(TargetProcessor): self._previous_colors.clear() self._previous_on.clear() + self._latest_entity_colors.clear() + self._ws_clients.clear() logger.info(f"HA light target stopped: {self._target_id}") def update_settings(self, settings) -> None: @@ -162,8 +167,24 @@ class HALightTargetProcessor(TargetProcessor): except Exception as e: logger.warning(f"HA light {self._target_id}: CSS swap failed: {e}") + # ── WebSocket clients ── + + def add_ws_client(self, ws: Any) -> None: + self._ws_clients.append(ws) + + def remove_ws_client(self, ws: Any) -> None: + if ws in self._ws_clients: + self._ws_clients.remove(ws) + + def supports_websocket(self) -> bool: + return True + def get_state(self) -> dict: uptime = time.monotonic() - self._start_time if self._start_time and self._is_running else 0 + entity_colors = { + eid: {"r": r, "g": g, "b": b, "hex": f"#{r:02x}{g:02x}{b:02x}"} + for eid, (r, g, b) in self._latest_entity_colors.items() + } return { "target_id": self._target_id, "processing": self._is_running, @@ -176,6 +197,7 @@ class HALightTargetProcessor(TargetProcessor): "fps_actual": self._update_rate if self._is_running else None, "fps_target": self._update_rate, "uptime_seconds": uptime, + "entity_colors": entity_colors, } def get_metrics(self) -> dict: @@ -244,6 +266,9 @@ class HALightTargetProcessor(TargetProcessor): avg = segment.mean(axis=0).astype(int) r, g, b = int(avg[0]), int(avg[1]), int(avg[2]) + # Cache for WS preview (always, even if HA call is skipped) + self._latest_entity_colors[mapping.entity_id] = (r, g, b) + # Calculate brightness (0-255) from max channel brightness = max(r, g, b) @@ -298,3 +323,23 @@ class HALightTargetProcessor(TargetProcessor): ) self._previous_on[entity_id] = False self._previous_colors.pop(entity_id, None) + + # Broadcast colors to WS clients + if self._ws_clients and self._latest_entity_colors: + await self._broadcast_entity_colors() + + async def _broadcast_entity_colors(self) -> None: + """Send current entity colors to all connected WS clients.""" + colors_payload = { + eid: {"r": r, "g": g, "b": b, "hex": f"#{r:02x}{g:02x}{b:02x}"} + for eid, (r, g, b) in self._latest_entity_colors.items() + } + message = json.dumps({"type": "colors_update", "colors": colors_payload}) + dead: List[Any] = [] + for ws in self._ws_clients: + try: + await ws.send_text(message) + except Exception: + dead.append(ws) + for ws in dead: + self._ws_clients.remove(ws) diff --git a/server/src/wled_controller/core/processing/kc_target_processor.py b/server/src/wled_controller/core/processing/kc_target_processor.py deleted file mode 100644 index 3c2ca95..0000000 --- a/server/src/wled_controller/core/processing/kc_target_processor.py +++ /dev/null @@ -1,489 +0,0 @@ -"""Key Colors target processor — extracts dominant colors from screen regions.""" - -from __future__ import annotations - -import asyncio -import collections -import json -import time -from datetime import datetime, timezone -from typing import Dict, List, Optional, Tuple - -import cv2 -import numpy as np - -from wled_controller.core.processing.live_stream import LiveStream -from wled_controller.core.capture.screen_capture import ( - calculate_average_color, - calculate_dominant_color, - calculate_median_color, -) -from wled_controller.core.processing.target_processor import ( - ProcessingMetrics, - TargetContext, - TargetProcessor, -) -from wled_controller.utils import get_logger -from wled_controller.utils.timer import high_resolution_timer - -logger = get_logger(__name__) - -KC_WORK_SIZE = (160, 90) # (width, height) — small enough for fast color calc - - -# --------------------------------------------------------------------------- -# CPU-bound frame processing (runs in thread pool via asyncio.to_thread) -# --------------------------------------------------------------------------- - -def _process_kc_frame(capture, rect_names, rect_bounds, calc_fn, prev_colors_arr, smoothing, brightness): - """All CPU-bound work for one KC frame. - - Returns (colors, colors_arr, timing_ms) where: - - colors is a dict {name: (r, g, b)} - - colors_arr is a (N, 3) float64 array for smoothing continuity - - timing_ms is a dict with per-stage timing in milliseconds. - """ - t0 = time.perf_counter() - - # Downsample to working resolution — 144x fewer pixels at 1080p - small = cv2.resize(capture.image, KC_WORK_SIZE, interpolation=cv2.INTER_AREA) - - # Extract colors for each rectangle from the small image - n = len(rect_names) - colors_arr = np.empty((n, 3), dtype=np.float64) - for i, (y1, y2, x1, x2) in enumerate(rect_bounds): - colors_arr[i] = calc_fn(small[y1:y2, x1:x2]) - - t1 = time.perf_counter() - - # Vectorized smoothing on (N, 3) array - if prev_colors_arr is not None and smoothing > 0: - colors_arr = colors_arr * (1 - smoothing) + prev_colors_arr * smoothing - - # Apply brightness scaling - if brightness < 1.0: - output_arr = colors_arr * brightness - else: - output_arr = colors_arr - - colors_u8 = np.clip(output_arr, 0, 255).astype(np.uint8) - t2 = time.perf_counter() - - # Build output dict - colors = {rect_names[i]: tuple(int(c) for c in colors_u8[i]) for i in range(n)} - - timing_ms = { - "calc_colors": (t1 - t0) * 1000, - "smooth": (t2 - t1) * 1000, - "total": (t2 - t0) * 1000, - } - return colors, colors_arr, timing_ms - - -# --------------------------------------------------------------------------- -# KCTargetProcessor -# --------------------------------------------------------------------------- - -class KCTargetProcessor(TargetProcessor): - """Extracts key colors from screen capture regions and broadcasts via WebSocket.""" - - def __init__( - self, - target_id: str, - picture_source_id: str, - settings, # KeyColorsSettings - ctx: TargetContext, - ): - super().__init__(target_id, ctx, picture_source_id) - self._settings = settings - self._brightness_vs_id = settings.brightness_value_source_id if settings else "" - - # Runtime state - self._live_stream: Optional[LiveStream] = None - self._value_stream = None # active brightness value stream - self._previous_colors: Optional[Dict[str, Tuple[int, int, int]]] = None - self._latest_colors: Optional[Dict[str, Tuple[int, int, int]]] = None - self._ws_clients: List = [] - self._resolved_target_fps: Optional[int] = None - self._resolved_rectangles = None - - # ----- Properties ----- - - @property - def settings(self): - return self._settings - - # ----- Lifecycle ----- - - async def start(self) -> None: - if self._is_running: - logger.debug(f"KC target {self._target_id} is already running") - return - - if not self._picture_source_id: - raise ValueError(f"KC target {self._target_id} has no picture source assigned") - - if not self._settings.pattern_template_id: - raise ValueError(f"KC target {self._target_id} has no pattern template assigned") - - # Resolve pattern template to get rectangles - try: - pattern_template = self._ctx.pattern_template_store.get_template( - self._settings.pattern_template_id - ) - except (ValueError, AttributeError): - raise ValueError( - f"Pattern template {self._settings.pattern_template_id} not found" - ) - - if not pattern_template.rectangles: - raise ValueError( - f"Pattern template {self._settings.pattern_template_id} has no rectangles" - ) - - self._resolved_rectangles = pattern_template.rectangles - - # Acquire live stream - try: - live_stream = await asyncio.to_thread( - self._ctx.live_stream_manager.acquire, self._picture_source_id - ) - self._live_stream = live_stream - self._resolved_target_fps = live_stream.target_fps - logger.info( - f"Acquired live stream for KC target {self._target_id} " - f"(picture_source={self._picture_source_id})" - ) - except Exception as e: - logger.error(f"Failed to initialize live stream for KC target {self._target_id}: {e}") - raise RuntimeError(f"Failed to initialize live stream: {e}") - - # Acquire value stream for brightness modulation (if configured) - if self._brightness_vs_id and self._ctx.value_stream_manager: - try: - self._value_stream = self._ctx.value_stream_manager.acquire( - self._brightness_vs_id - ) - except Exception as e: - logger.warning(f"Failed to acquire value stream {self._brightness_vs_id}: {e}") - self._value_stream = None - - # Reset metrics - self._metrics = ProcessingMetrics(start_time=datetime.now(timezone.utc)) - self._previous_colors = None - self._latest_colors = None - - # Start processing task - self._is_running = True - self._task = asyncio.create_task(self._processing_loop()) - - logger.info(f"Started KC processing for target {self._target_id}") - self._ctx.fire_event({"type": "state_change", "target_id": self._target_id, "processing": True}) - - async def stop(self) -> None: - if not self._is_running: - logger.warning(f"KC processing not running for target {self._target_id}") - return - - self._is_running = False - - # Cancel task - if self._task: - self._task.cancel() - try: - await self._task - except asyncio.CancelledError: - logger.debug("KC target processor task cancelled") - pass - self._task = None - - # Release live stream - if self._live_stream: - try: - self._ctx.live_stream_manager.release(self._picture_source_id) - except Exception as e: - logger.warning(f"Error releasing live stream for KC target: {e}") - self._live_stream = None - - # Release value stream - if self._value_stream is not None and self._ctx.value_stream_manager: - try: - self._ctx.value_stream_manager.release(self._brightness_vs_id) - except Exception as e: - logger.warning(f"Error releasing value stream: {e}") - self._value_stream = None - - logger.info(f"Stopped KC processing for target {self._target_id}") - self._ctx.fire_event({"type": "state_change", "target_id": self._target_id, "processing": False}) - - # ----- Settings ----- - - def update_settings(self, settings) -> None: - self._settings = settings - # Keep _brightness_vs_id in sync (hot-swap handled separately) - self._brightness_vs_id = settings.brightness_value_source_id if settings else "" - logger.info(f"Updated KC target settings: {self._target_id}") - - def update_brightness_value_source(self, vs_id: str) -> None: - """Hot-swap the brightness value source for a running KC target.""" - old_vs_id = self._brightness_vs_id - self._brightness_vs_id = vs_id - vs_mgr = self._ctx.value_stream_manager - - if not self._is_running or vs_mgr is None: - return - - # Release old stream - if self._value_stream is not None and old_vs_id: - try: - vs_mgr.release(old_vs_id) - except Exception as e: - logger.warning(f"Error releasing old value stream {old_vs_id}: {e}") - self._value_stream = None - - # Acquire new stream - if vs_id: - try: - self._value_stream = vs_mgr.acquire(vs_id) - except Exception as e: - logger.warning(f"Failed to acquire value stream {vs_id}: {e}") - self._value_stream = None - - logger.info(f"Hot-swapped brightness VS for KC target {self._target_id}: {old_vs_id} -> {vs_id}") - - # ----- State / Metrics ----- - - def get_state(self) -> dict: - metrics = self._metrics - return { - "target_id": self._target_id, - "processing": self._is_running, - "fps_actual": round(metrics.fps_actual, 1) if self._is_running else None, - "fps_potential": metrics.fps_potential if self._is_running else None, - "fps_target": self._settings.fps, - "frames_skipped": metrics.frames_skipped if self._is_running else None, - "frames_keepalive": metrics.frames_keepalive if self._is_running else None, - "fps_current": metrics.fps_current if self._is_running else None, - "timing_calc_colors_ms": round(metrics.timing_calc_colors_ms, 1) if self._is_running else None, - "timing_smooth_ms": round(metrics.timing_smooth_ms, 1) if self._is_running else None, - "timing_broadcast_ms": round(metrics.timing_broadcast_ms, 1) if self._is_running else None, - "timing_total_ms": round(metrics.timing_total_ms, 1) if self._is_running else None, - "last_update": metrics.last_update, - "errors": [metrics.last_error] if metrics.last_error else [], - "brightness_value_source_id": self._brightness_vs_id, - } - - def get_metrics(self) -> dict: - metrics = self._metrics - uptime = 0.0 - if metrics.start_time and self._is_running: - uptime = (datetime.now(timezone.utc) - metrics.start_time).total_seconds() - - return { - "target_id": self._target_id, - "processing": self._is_running, - "fps_actual": round(metrics.fps_actual, 1), - "fps_target": self._settings.fps, - "uptime_seconds": round(uptime, 1), - "frames_processed": metrics.frames_processed, - "errors_count": metrics.errors_count, - "last_error": metrics.last_error, - "last_update": metrics.last_update.isoformat() if metrics.last_update else None, - } - - # ----- WebSocket ----- - - def supports_websocket(self) -> bool: - return True - - def add_ws_client(self, ws) -> None: - self._ws_clients.append(ws) - - def remove_ws_client(self, ws) -> None: - if ws in self._ws_clients: - self._ws_clients.remove(ws) - - def get_latest_colors(self) -> Dict[str, Tuple[int, int, int]]: - return self._latest_colors or {} - - # ----- Private: processing loop ----- - - async def _processing_loop(self) -> None: - """Main processing loop for key-colors extraction.""" - target_fps = self._settings.fps - - # Lookup table for interpolation mode → function (used per-frame from live settings) - calc_fns = { - "average": calculate_average_color, - "median": calculate_median_color, - "dominant": calculate_dominant_color, - } - - frame_time = 1.0 / target_fps - fps_samples: collections.deque = collections.deque(maxlen=10) - timing_samples: collections.deque = collections.deque(maxlen=10) - prev_frame_time_stamp = time.perf_counter() - prev_capture = None - last_broadcast_time = 0.0 - send_timestamps: collections.deque = collections.deque() - - rectangles = self._resolved_rectangles - - # Pre-compute pixel bounds at working resolution (160x90) - kc_w, kc_h = KC_WORK_SIZE - rect_names = [r.name for r in rectangles] - rect_bounds = [] - for rect in rectangles: - px_x = max(0, int(rect.x * kc_w)) - px_y = max(0, int(rect.y * kc_h)) - px_w = max(1, int(rect.width * kc_w)) - px_h = max(1, int(rect.height * kc_h)) - px_x = min(px_x, kc_w - 1) - px_y = min(px_y, kc_h - 1) - px_w = min(px_w, kc_w - px_x) - px_h = min(px_h, kc_h - px_y) - rect_bounds.append((px_y, px_y + px_h, px_x, px_x + px_w)) - prev_colors_arr = None - - logger.info( - f"KC processing loop started for target {self._target_id} " - f"(fps={target_fps}, rects={len(rectangles)})" - ) - - try: - with high_resolution_timer(): - while self._is_running: - loop_start = time.perf_counter() - - try: - capture = self._live_stream.get_latest_frame() - - if capture is None: - await asyncio.sleep(frame_time) - continue - - # Skip processing if the frame hasn't changed - if capture is prev_capture: - # Keepalive: re-broadcast last colors - if self._latest_colors and (loop_start - last_broadcast_time) >= 1.0: - await self._broadcast_colors(self._latest_colors) - last_broadcast_time = time.perf_counter() - send_timestamps.append(last_broadcast_time) - self._metrics.frames_keepalive += 1 - self._metrics.frames_skipped += 1 - now_ts = time.perf_counter() - while send_timestamps and send_timestamps[0] < now_ts - 1.0: - send_timestamps.popleft() - self._metrics.fps_current = len(send_timestamps) - await asyncio.sleep(frame_time) - continue - prev_capture = capture - - # Read settings fresh each frame so hot updates (brightness, - # smoothing, interpolation_mode) take effect without restart. - s = self._settings - calc_fn = calc_fns.get(s.interpolation_mode, calculate_average_color) - - # Effective brightness: static setting * value stream - eff_brightness = s.brightness - vs = self._value_stream - if vs is not None: - eff_brightness *= vs.get_value() - - # CPU-bound work in thread pool - colors, colors_arr, frame_timing = await asyncio.to_thread( - _process_kc_frame, - capture, rect_names, rect_bounds, calc_fn, - prev_colors_arr, s.smoothing, eff_brightness, - ) - - prev_colors_arr = colors_arr - self._latest_colors = dict(colors) - - # Broadcast to WebSocket clients - t_broadcast_start = time.perf_counter() - await self._broadcast_colors(colors) - broadcast_ms = (time.perf_counter() - t_broadcast_start) * 1000 - last_broadcast_time = time.perf_counter() - send_timestamps.append(last_broadcast_time) - - # Per-stage timing (rolling average over last 10 frames) - frame_timing["broadcast"] = broadcast_ms - timing_samples.append(frame_timing) - n = len(timing_samples) - self._metrics.timing_calc_colors_ms = sum(s["calc_colors"] for s in timing_samples) / n - self._metrics.timing_smooth_ms = sum(s["smooth"] for s in timing_samples) / n - self._metrics.timing_broadcast_ms = sum(s["broadcast"] for s in timing_samples) / n - self._metrics.timing_total_ms = sum(s["total"] for s in timing_samples) / n + broadcast_ms - - # Update metrics - self._metrics.frames_processed += 1 - self._metrics.last_update = datetime.now(timezone.utc) - - # Calculate actual FPS - now = time.perf_counter() - interval = now - prev_frame_time_stamp - prev_frame_time_stamp = now - fps_samples.append(1.0 / interval if interval > 0 else 0) - self._metrics.fps_actual = sum(fps_samples) / len(fps_samples) - - # Potential FPS - processing_time = now - loop_start - self._metrics.fps_potential = 1.0 / processing_time if processing_time > 0 else 0 - - # fps_current - while send_timestamps and send_timestamps[0] < now - 1.0: - send_timestamps.popleft() - self._metrics.fps_current = len(send_timestamps) - - except Exception as e: - self._metrics.errors_count += 1 - self._metrics.last_error = str(e) - logger.error(f"KC processing error for {self._target_id}: {e}", exc_info=True) - - # Throttle to target FPS - elapsed = time.perf_counter() - loop_start - remaining = frame_time - elapsed - if remaining > 0: - await asyncio.sleep(remaining) - - except asyncio.CancelledError: - logger.info(f"KC processing loop cancelled for target {self._target_id}") - raise - except Exception as e: - logger.error(f"Fatal error in KC processing loop for target {self._target_id}: {e}") - self._is_running = False - self._ctx.fire_event({"type": "state_change", "target_id": self._target_id, "processing": False, "crashed": True}) - raise - finally: - logger.info(f"KC processing loop ended for target {self._target_id}") - - async def _broadcast_colors(self, colors: Dict[str, Tuple[int, int, int]]) -> None: - """Broadcast extracted colors to WebSocket clients (concurrent sends).""" - if not self._ws_clients: - return - - message = json.dumps({ - "type": "colors_update", - "target_id": self._target_id, - "colors": { - name: {"r": c[0], "g": c[1], "b": c[2]} - for name, c in colors.items() - }, - "timestamp": datetime.now(timezone.utc).isoformat(), - }) - - async def _send_safe(ws): - try: - await ws.send_text(message) - return True - except Exception as e: - logger.debug("KC WS send failed: %s", e) - return False - - clients = list(self._ws_clients) - results = await asyncio.gather(*[_send_safe(ws) for ws in clients]) - - for ws, ok in zip(clients, results): - if not ok and ws in self._ws_clients: - self._ws_clients.remove(ws) diff --git a/server/src/wled_controller/core/processing/processor_manager.py b/server/src/wled_controller/core/processing/processor_manager.py index e4981df..7edd5ac 100644 --- a/server/src/wled_controller/core/processing/processor_manager.py +++ b/server/src/wled_controller/core/processing/processor_manager.py @@ -498,10 +498,6 @@ class ProcessorManager(AutoRestartMixin, DeviceHealthMixin, DeviceTestModeMixin) del self._processors[target_id] logger.info(f"Unregistered target {target_id}") - # Backward-compat alias - def remove_kc_target(self, target_id: str) -> None: - self.remove_target(target_id) - # ===== UNIFIED TARGET OPERATIONS ===== def update_target_settings(self, target_id: str, settings): @@ -774,10 +770,15 @@ class ProcessorManager(AutoRestartMixin, DeviceHealthMixin, DeviceTestModeMixin) # ===== WEBSOCKET (delegates to processor) ===== - def add_kc_ws_client(self, target_id: str, ws) -> None: + def add_ha_light_ws_client(self, target_id: str, ws) -> None: proc = self._get_processor(target_id) proc.add_ws_client(ws) + def remove_ha_light_ws_client(self, target_id: str, ws) -> None: + proc = self._processors.get(target_id) + if proc: + proc.remove_ws_client(ws) + def add_led_preview_client(self, target_id: str, ws) -> None: proc = self._get_processor(target_id) proc.add_led_preview_client(ws) diff --git a/server/src/wled_controller/core/processing/target_processor.py b/server/src/wled_controller/core/processing/target_processor.py index 67b8eb0..f39739b 100644 --- a/server/src/wled_controller/core/processing/target_processor.py +++ b/server/src/wled_controller/core/processing/target_processor.py @@ -1,7 +1,7 @@ """Abstract base class for target processors. A TargetProcessor encapsulates the processing loop and state for a single -output target. Concrete subclasses (WledTargetProcessor, KCTargetProcessor) +output target. Concrete subclasses (WledTargetProcessor, HALightTargetProcessor) implement the target-specific capture→process→output pipeline. ProcessorManager creates and owns TargetProcessor instances, delegating diff --git a/server/src/wled_controller/main.py b/server/src/wled_controller/main.py index 9ec8d99..e9fe234 100644 --- a/server/src/wled_controller/main.py +++ b/server/src/wled_controller/main.py @@ -233,7 +233,6 @@ async def lifespan(app: FastAPI): logger.info(f"Registered {len(devices)} devices for health monitoring") - # Migrate KC targets → key_colors CSS sources # Register output targets in processor manager targets = output_target_store.get_all_targets() registered_targets = 0 diff --git a/server/src/wled_controller/static/css/modal.css b/server/src/wled_controller/static/css/modal.css index 5199dbd..2c2f2cb 100644 --- a/server/src/wled_controller/static/css/modal.css +++ b/server/src/wled_controller/static/css/modal.css @@ -1553,6 +1553,35 @@ height: 16px; } +/* HA Light color swatches */ +.ha-light-swatches { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 8px; +} +.ha-light-swatch { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 8px; + border-radius: 4px; + background: var(--bg-color); + border: 1px solid var(--border-color); + font-size: 0.75rem; +} +.ha-light-swatch .swatch-color { + display: inline-block; + width: 16px; + height: 16px; + border-radius: 3px; + border: 1px solid rgba(255,255,255,0.2); + transition: background 0.3s; +} +.ha-light-swatch .swatch-label { + opacity: 0.8; +} + .btn-remove-mapping:hover { opacity: 1; } diff --git a/server/src/wled_controller/static/js/app.ts b/server/src/wled_controller/static/js/app.ts index d9ecb8c..418e158 100644 --- a/server/src/wled_controller/static/js/app.ts +++ b/server/src/wled_controller/static/js/app.ts @@ -35,7 +35,7 @@ import { closeTutorial, tutorialNext, tutorialPrev, } from './features/tutorials.ts'; -// Layer 4: devices, dashboard, streams, kc-targets, pattern-templates, automations +// Layer 4: devices, dashboard, streams, pattern-templates, automations import { showSettings, closeDeviceSettingsModal, forceCloseDeviceSettingsModal, saveDeviceSettings, updateBrightnessLabel, saveCardBrightness, diff --git a/server/src/wled_controller/static/js/core/command-palette.ts b/server/src/wled_controller/static/js/core/command-palette.ts index fa803af..7639245 100644 --- a/server/src/wled_controller/static/js/core/command-palette.ts +++ b/server/src/wled_controller/static/js/core/command-palette.ts @@ -6,7 +6,7 @@ import { fetchWithAuth, escapeHtml } from './api.ts'; import { t } from './i18n.ts'; import { navigateToCard } from './navigation.ts'; import { - getTargetTypeIcon, getPictureSourceIcon, getColorStripIcon, getAudioSourceIcon, + getPictureSourceIcon, getColorStripIcon, getAudioSourceIcon, ICON_DEVICE, ICON_TARGET, ICON_AUTOMATION, ICON_VALUE_SOURCE, ICON_SCENE, ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_PATTERN_TEMPLATE, ICON_CSPT, ICON_CLOCK, } from './icons.ts'; @@ -46,17 +46,10 @@ function _buildItems(results: any[], states: any = {}) { _mapEntities(targets, tgt => { const running = !!states[tgt.id]?.processing; - if (tgt.target_type === 'key_colors') { - items.push({ - name: tgt.name, detail: 'key_colors', group: 'kc_targets', icon: getTargetTypeIcon('key_colors'), - nav: ['targets', 'kc-targets', 'kc-targets', 'data-kc-target-id', tgt.id], running, - }); - } else { - items.push({ - name: tgt.name, detail: tgt.target_type, group: 'targets', icon: ICON_TARGET, - nav: ['targets', 'led-targets', 'led-targets', 'data-target-id', tgt.id], running, - }); - } + items.push({ + name: tgt.name, detail: tgt.target_type, group: 'targets', icon: ICON_TARGET, + nav: ['targets', 'led-targets', 'led-targets', 'data-target-id', tgt.id], running, + }); // Action item: toggle start/stop const actionItem: any = { name: tgt.name, group: 'actions', @@ -219,7 +212,7 @@ async function _fetchAllEntities() { const _groupOrder = [ 'actions', - 'devices', 'targets', 'kc_targets', 'css', 'cspt', 'automations', + 'devices', 'targets', 'css', 'cspt', 'automations', 'streams', 'capture_templates', 'pp_templates', 'pattern_templates', 'audio', 'value', 'scenes', 'sync_clocks', ]; diff --git a/server/src/wled_controller/static/js/core/graph-nodes.ts b/server/src/wled_controller/static/js/core/graph-nodes.ts index 7d75821..0abc297 100644 --- a/server/src/wled_controller/static/js/core/graph-nodes.ts +++ b/server/src/wled_controller/static/js/core/graph-nodes.ts @@ -105,7 +105,7 @@ const SUBTYPE_ICONS = { adaptive_time: P.clock, adaptive_scene: P.cloudSun, daylight: P.sun, }, audio_source: { mono: P.mic, multichannel: P.volume2 }, - output_target: { led: P.lightbulb, wled: P.lightbulb, key_colors: P.palette }, + output_target: { led: P.lightbulb, wled: P.lightbulb, ha_light: P.lightbulb }, }; function svgEl(tag: string, attrs: Record = {}): SVGElement { @@ -413,7 +413,7 @@ function _createOverlay(node: GraphNode, nodeWidth: number, callbacks: NodeCallb } // Test button for applicable kinds - if (TEST_KINDS.has(node.kind) || (node.kind === 'output_target' && node.subtype === 'key_colors')) { + if (TEST_KINDS.has(node.kind)) { btns.push({ svgPath: P.flaskConical, action: 'test', cls: '' }); } diff --git a/server/src/wled_controller/static/js/core/icons.ts b/server/src/wled_controller/static/js/core/icons.ts index 36c1d1c..f5d90cb 100644 --- a/server/src/wled_controller/static/js/core/icons.ts +++ b/server/src/wled_controller/static/js/core/icons.ts @@ -14,7 +14,7 @@ import * as P from './icon-paths.ts'; const _svg = (d: string): string => `${d}`; // ── Type-resolution maps (private) ────────────────────────── -const _targetTypeIcons = { led: _svg(P.lightbulb), wled: _svg(P.lightbulb), key_colors: _svg(P.palette) }; +const _targetTypeIcons = { led: _svg(P.lightbulb), wled: _svg(P.lightbulb), ha_light: _svg(P.lightbulb) }; const _pictureSourceTypeIcons = { raw: _svg(P.monitor), processed: _svg(P.palette), static_image: _svg(P.image), video: _svg(P.film) }; const _colorStripTypeIcons = { picture_advanced: _svg(P.monitor), diff --git a/server/src/wled_controller/static/js/core/state.ts b/server/src/wled_controller/static/js/core/state.ts index f2699f9..f39af09 100644 --- a/server/src/wled_controller/static/js/core/state.ts +++ b/server/src/wled_controller/static/js/core/state.ts @@ -24,18 +24,6 @@ export function setAuthRequired(v: boolean) { authRequired = v; } export let refreshInterval: ReturnType | null = null; export function setRefreshInterval(v: ReturnType | null) { refreshInterval = v; } -export let kcTestAutoRefresh: ReturnType | null = null; -export function setKcTestAutoRefresh(v: ReturnType | null) { kcTestAutoRefresh = v; } - -export let kcTestTargetId: string | null = null; -export function setKcTestTargetId(v: string | null) { kcTestTargetId = v; } - -export let kcTestWs: WebSocket | null = null; -export function setKcTestWs(v: WebSocket | null) { kcTestWs = v; } - -export let kcTestFps = 3; -export function setKcTestFps(v: number) { kcTestFps = v; } - export let _cachedDisplays: Display[] | null = null; export let _displayPickerCallback: ((index: number, display?: Display | null) => void) | null = null; @@ -127,13 +115,6 @@ export function set_lastValidatedImageSource(v: string) { _lastValidatedImageSou export let _targetEditorDevices: Device[] = []; export function set_targetEditorDevices(v: Device[]) { _targetEditorDevices = v; } -// KC editor state -export let _kcNameManuallyEdited = false; -export function set_kcNameManuallyEdited(v: boolean) { _kcNameManuallyEdited = v; } - -// KC WebSockets -export const kcWebSockets: Record = {}; - // LED Preview WebSockets export const ledPreviewWebSockets: Record = {}; diff --git a/server/src/wled_controller/static/js/core/ui.ts b/server/src/wled_controller/static/js/core/ui.ts index 39d7490..9ad0841 100644 --- a/server/src/wled_controller/static/js/core/ui.ts +++ b/server/src/wled_controller/static/js/core/ui.ts @@ -2,7 +2,7 @@ * UI utilities — modal helpers, lightbox, toast, confirm. */ -import { kcTestAutoRefresh, setKcTestAutoRefresh, setKcTestTargetId, kcTestWs, setKcTestWs, confirmResolve, setConfirmResolve } from './state.ts'; +import { confirmResolve, setConfirmResolve } from './state.ts'; import { API_BASE, getHeaders } from './api.ts'; import { t } from './i18n.ts'; @@ -101,8 +101,6 @@ export function openLightbox(imageSrc: string, statsHtml?: string) { export function closeLightbox(event?: Event) { if (event && event.target && (event.target as HTMLElement).closest('.lightbox-content')) return; - // Stop KC test WS if running - stopKCTestAutoRefresh(); const lightbox = document.getElementById('image-lightbox')!; lightbox.classList.remove('active'); const img = document.getElementById('lightbox-image') as HTMLImageElement; @@ -115,18 +113,6 @@ export function closeLightbox(event?: Event) { unlockBody(); } -export function stopKCTestAutoRefresh() { - if (kcTestAutoRefresh) { - clearInterval(kcTestAutoRefresh); - setKcTestAutoRefresh(null); - } - if (kcTestWs) { - try { kcTestWs.close(); } catch (_) {} - setKcTestWs(null); - } - setKcTestTargetId(null); -} - export function showToast(message: string, type = 'info') { const toast = document.getElementById('toast')!; toast.textContent = message; diff --git a/server/src/wled_controller/static/js/features/graph-editor.ts b/server/src/wled_controller/static/js/features/graph-editor.ts index c9d39ab..e7b7261 100644 --- a/server/src/wled_controller/static/js/features/graph-editor.ts +++ b/server/src/wled_controller/static/js/features/graph-editor.ts @@ -1528,7 +1528,7 @@ function _onTestNode(node: any) { value_source: () => _w.testValueSource?.(node.id), color_strip_source: () => _w.testColorStrip?.(node.id), cspt: () => _w.testCSPT?.(node.id), - output_target: () => _w.testKCTarget?.(node.id), + output_target: undefined, }; fnMap[node.kind]?.(); } diff --git a/server/src/wled_controller/static/js/features/ha-light-targets.ts b/server/src/wled_controller/static/js/features/ha-light-targets.ts index 9391bf2..1c7927f 100644 --- a/server/src/wled_controller/static/js/features/ha-light-targets.ts +++ b/server/src/wled_controller/static/js/features/ha-light-targets.ts @@ -488,6 +488,9 @@ export function createHALightTargetCard(target: any, haSourceMap: Record${state.ha_connected ? ICON_OK : ICON_WARNING} +
+ ${_renderEntitySwatches(state.entity_colors || {}, target.ha_light_mappings || [])} +
` : ''} `, actions: ` @@ -560,6 +563,78 @@ export function initHALightTargetDelegation(container: HTMLElement): void { }); } +// ── Entity color swatches ── + +function _renderEntitySwatches(entityColors: Record, mappings: any[]): string { + if (!mappings.length) return ''; + return mappings.map(m => { + const c = entityColors[m.entity_id]; + const bg = c ? c.hex : '#333'; + const label = m.entity_id.replace('light.', ''); + return `
+ + ${escapeHtml(label)} +
`; + }).join(''); +} + +// ── WebSocket color preview ── + +const _haLightWS: Record = {}; + +export function connectHALightWS(targetId: string): void { + if (_haLightWS[targetId]) return; + const loc = window.location; + const wsProto = loc.protocol === 'https:' ? 'wss:' : 'ws:'; + const apiKey = (window as any).apiKey || localStorage.getItem('wled_api_key') || ''; + const url = `${wsProto}//${loc.host}/api/v1/output-targets/${targetId}/ha-light/ws?token=${encodeURIComponent(apiKey)}`; + + const ws = new WebSocket(url); + _haLightWS[targetId] = ws; + + ws.onmessage = (ev) => { + try { + const data = JSON.parse(ev.data); + if (data.type === 'colors_update') { + _updateSwatchColors(targetId, data.colors); + } + } catch {} + }; + + ws.onclose = () => { + delete _haLightWS[targetId]; + }; + + ws.onerror = () => { + delete _haLightWS[targetId]; + }; +} + +export function disconnectHALightWS(targetId: string): void { + const ws = _haLightWS[targetId]; + if (ws) { + ws.close(); + delete _haLightWS[targetId]; + } +} + +export function disconnectAllHALightWS(): void { + for (const id of Object.keys(_haLightWS)) { + disconnectHALightWS(id); + } +} + +function _updateSwatchColors(targetId: string, colors: Record): void { + const container = document.querySelector(`[data-ha-swatches="${targetId}"]`); + if (!container) return; + for (const [entityId, c] of Object.entries(colors)) { + const swatch = container.querySelector(`[data-entity="${entityId}"] .swatch-color`) as HTMLElement | null; + if (swatch) { + swatch.style.background = (c as any).hex; + } + } +} + // ── Expose to global scope ── window.showHALightEditor = showHALightEditor; diff --git a/server/src/wled_controller/static/js/features/kc-targets.ts b/server/src/wled_controller/static/js/features/kc-targets.ts deleted file mode 100644 index e40f62f..0000000 --- a/server/src/wled_controller/static/js/features/kc-targets.ts +++ /dev/null @@ -1,841 +0,0 @@ -/** - * Key Colors targets — cards, test lightbox, editor, WebSocket live colors. - */ - -import { - kcTestAutoRefresh, setKcTestAutoRefresh, - kcTestTargetId, setKcTestTargetId, - kcTestWs, setKcTestWs, - kcTestFps, setKcTestFps, - _kcNameManuallyEdited, set_kcNameManuallyEdited, - kcWebSockets, - PATTERN_RECT_BORDERS, - _cachedValueSources, valueSourcesCache, streamsCache, - outputTargetsCache, patternTemplatesCache, -} from '../core/state.ts'; -import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.ts'; -import { t } from '../core/i18n.ts'; -import { lockBody, showToast, showConfirm, formatUptime, formatCompact, desktopFocus } from '../core/ui.ts'; -import { Modal } from '../core/modal.ts'; -import { - getValueSourceIcon, getPictureSourceIcon, - ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_START, ICON_STOP, - ICON_LINK_SOURCE, ICON_PATTERN_TEMPLATE, ICON_FPS, ICON_PALETTE, -} from '../core/icons.ts'; -import * as P from '../core/icon-paths.ts'; -import { wrapCard } from '../core/card-colors.ts'; -import { TagInput, renderTagChips } from '../core/tag-input.ts'; -import { IconSelect } from '../core/icon-select.ts'; -import { EntitySelect } from '../core/entity-palette.ts'; -import type { OutputTarget } from '../types.ts'; - -let _kcTagsInput: any = null; - -class KCEditorModal extends Modal { - constructor() { - super('kc-editor-modal'); - } - - snapshotValues() { - return { - name: (document.getElementById('kc-editor-name') as HTMLInputElement).value, - source: (document.getElementById('kc-editor-source') as HTMLSelectElement).value, - fps: (document.getElementById('kc-editor-fps') as HTMLInputElement).value, - interpolation: (document.getElementById('kc-editor-interpolation') as HTMLSelectElement).value, - smoothing: (document.getElementById('kc-editor-smoothing') as HTMLInputElement).value, - patternTemplateId: (document.getElementById('kc-editor-pattern-template') as HTMLSelectElement).value, - brightness_vs: (document.getElementById('kc-editor-brightness-vs') as HTMLSelectElement).value, - tags: JSON.stringify(_kcTagsInput ? _kcTagsInput.getValue() : []), - }; - } -} - -const kcEditorModal = new KCEditorModal(); - -/* ── Visual selectors ─────────────────────────────────────────── */ - -const _icon = (d: any) => `${d}`; - -let _kcColorModeIconSelect: any = null; -let _kcSourceEntitySelect: any = null; -let _kcPatternEntitySelect: any = null; -let _kcBrightnessEntitySelect: any = null; - -// Inline SVG previews for color modes -const _COLOR_MODE_SVG = { - average: '', - median: '', - dominant: '', -}; - -function _ensureColorModeIconSelect() { - const sel = document.getElementById('kc-editor-interpolation'); - if (!sel) return; - const items = [ - { value: 'average', icon: _COLOR_MODE_SVG.average, label: t('kc.interpolation.average'), desc: t('kc.interpolation.average.desc') }, - { value: 'median', icon: _COLOR_MODE_SVG.median, label: t('kc.interpolation.median'), desc: t('kc.interpolation.median.desc') }, - { value: 'dominant', icon: _COLOR_MODE_SVG.dominant, label: t('kc.interpolation.dominant'), desc: t('kc.interpolation.dominant.desc') }, - ]; - if (_kcColorModeIconSelect) { _kcColorModeIconSelect.updateItems(items); return; } - _kcColorModeIconSelect = new IconSelect({ target: sel, items, columns: 3 } as any); -} - -function _ensureSourceEntitySelect(sources: any) { - const sel = document.getElementById('kc-editor-source'); - if (!sel) return; - if (_kcSourceEntitySelect) _kcSourceEntitySelect.destroy(); - if (sources.length > 0) { - _kcSourceEntitySelect = new EntitySelect({ - target: sel, - getItems: () => sources.map((s: any) => ({ - value: s.id, - label: s.name, - icon: getPictureSourceIcon(s.stream_type), - desc: s.stream_type, - })), - placeholder: t('palette.search'), - } as any); - } -} - -function _ensurePatternEntitySelect(patTemplates: any) { - const sel = document.getElementById('kc-editor-pattern-template'); - if (!sel) return; - if (_kcPatternEntitySelect) _kcPatternEntitySelect.destroy(); - if (patTemplates.length > 0) { - _kcPatternEntitySelect = new EntitySelect({ - target: sel, - getItems: () => patTemplates.map((pt: any) => { - const rectCount = (pt.rectangles || []).length; - return { - value: pt.id, - label: pt.name, - icon: _icon(P.fileText), - desc: `${rectCount} rect${rectCount !== 1 ? 's' : ''}`, - }; - }), - placeholder: t('palette.search'), - } as any); - } -} - -function _ensureBrightnessEntitySelect() { - const sel = document.getElementById('kc-editor-brightness-vs'); - if (!sel) return; - if (_kcBrightnessEntitySelect) _kcBrightnessEntitySelect.destroy(); - if (_cachedValueSources.length > 0) { - _kcBrightnessEntitySelect = new EntitySelect({ - target: sel, - getItems: () => { - const items = [{ value: '', label: t('kc.brightness_vs.none'), icon: _icon(P.sunDim), desc: '' }]; - return items.concat(_cachedValueSources.map((vs: any) => ({ - value: vs.id, - label: vs.name, - icon: getValueSourceIcon(vs.source_type), - desc: vs.source_type, - }))); - }, - placeholder: t('palette.search'), - } as any); - } -} - -export function patchKCTargetMetrics(target: any) { - const card = document.querySelector(`[data-kc-target-id="${CSS.escape(target.id)}"]`); - if (!card) return; - const state = target.state || {}; - const metrics = target.metrics || {}; - - const fpsActual = card.querySelector('[data-tm="fps-actual"]'); - if (fpsActual) fpsActual.textContent = state.fps_actual?.toFixed(1) || '0.0'; - - const fpsCurrent = card.querySelector('[data-tm="fps-current"]'); - if (fpsCurrent) fpsCurrent.textContent = state.fps_current ?? '-'; - - const fpsTarget = card.querySelector('[data-tm="fps-target"]'); - if (fpsTarget) fpsTarget.textContent = state.fps_target || 0; - - const frames = card.querySelector('[data-tm="frames"]') as HTMLElement; - if (frames) { frames.textContent = formatCompact(metrics.frames_processed || 0); frames.title = String(metrics.frames_processed || 0); } - - const keepalive = card.querySelector('[data-tm="keepalive"]') as HTMLElement; - if (keepalive) { keepalive.textContent = formatCompact(state.frames_keepalive ?? 0); keepalive.title = String(state.frames_keepalive ?? 0); } - - const errors = card.querySelector('[data-tm="errors"]') as HTMLElement; - if (errors) { errors.textContent = formatCompact(metrics.errors_count || 0); errors.title = String(metrics.errors_count || 0); } - - const uptime = card.querySelector('[data-tm="uptime"]'); - if (uptime) uptime.textContent = formatUptime(metrics.uptime_seconds); - - const timing = card.querySelector('[data-tm="timing"]'); - if (timing && state.timing_total_ms != null) { - timing.innerHTML = ` -
-
${t('device.metrics.timing')}
-
${state.timing_total_ms}ms
-
-
- - - -
-
- calc ${state.timing_calc_colors_ms}ms - smooth ${state.timing_smooth_ms}ms - broadcast ${state.timing_broadcast_ms}ms -
`; - } -} - -export function createKCTargetCard(target: OutputTarget & { state?: any; metrics?: any; latestColors?: any }, sourceMap: Record, patternTemplateMap: Record, valueSourceMap: Record) { - const state = target.state || {}; - const kcSettings = target.key_colors_settings ?? {} as Partial; - - const isProcessing = state.processing || false; - const brightness = kcSettings.brightness ?? 1.0; - const brightnessInt = Math.round(brightness * 255); - - const source = sourceMap[target.picture_source_id!]; - const sourceName = source ? source.name : (target.picture_source_id || 'No source'); - const patTmpl = patternTemplateMap[kcSettings.pattern_template_id!]; - const patternName = patTmpl ? patTmpl.name : 'No pattern'; - const rectCount = patTmpl ? (patTmpl.rectangles || []).length : 0; - - const bvsId = kcSettings.brightness_value_source_id || ''; - const bvs = bvsId && valueSourceMap ? valueSourceMap[bvsId] : null; - - // Render initial color swatches from pre-fetched REST data - let swatchesHtml = ''; - const latestColors = target.latestColors && target.latestColors.colors; - if (isProcessing && latestColors && Object.keys(latestColors).length > 0) { - swatchesHtml = Object.entries(latestColors).map(([name, color]: [string, any]) => ` -
-
- ${escapeHtml(name)} -
- `).join(''); - } else if (isProcessing) { - swatchesHtml = `${t('kc.colors.none')}`; - } - - return wrapCard({ - dataAttr: 'data-kc-target-id', - id: target.id, - removeOnclick: `deleteKCTarget('${target.id}')`, - removeTitle: t('common.delete'), - content: ` -
-
- ${escapeHtml(target.name)} -
-
-
- ${ICON_LINK_SOURCE} ${escapeHtml(sourceName)} - ${ICON_PATTERN_TEMPLATE} ${escapeHtml(patternName)} - ▭ ${rectCount} rect${rectCount !== 1 ? 's' : ''} - ${ICON_FPS} ${kcSettings.fps ?? 10} - ${bvs ? `${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}` : ''} -
- ${renderTagChips(target.tags)} -
- -
-
- ${swatchesHtml} -
- ${isProcessing ? ` -
-
-
-
${t('device.metrics.actual_fps')}
-
---
-
-
-
${t('device.metrics.current_fps')}
-
---
-
-
-
${t('device.metrics.target_fps')}
-
---
-
-
-
${t('device.metrics.frames')}
-
---
-
-
-
${t('device.metrics.keepalive')}
-
---
-
-
-
${t('device.metrics.errors')}
-
---
-
-
-
${t('device.metrics.uptime')}
-
---
-
-
- ${state.timing_total_ms != null ? ` -
- ` : ''} -
- ` : ''}`, - actions: ` - ${isProcessing ? ` - - ` : ` - - `} - - - `, - }); -} - -// ===== KEY COLORS TEST ===== - -function _openKCTestWs(targetId: any, fps: any, previewWidth = 480) { - // Close any existing WS - if (kcTestWs) { - try { kcTestWs.close(); } catch (_) {} - setKcTestWs(null); - } - - const key = localStorage.getItem('wled_api_key'); - if (!key) return; - - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}${API_BASE}/output-targets/${targetId}/test/ws?token=${encodeURIComponent(key)}&fps=${fps}&preview_width=${previewWidth}`; - - const ws = new WebSocket(wsUrl); - - ws.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - if (data.type === 'frame') { - // Hide spinner on first frame - const spinner = document.querySelector('.lightbox-spinner') as HTMLElement; - if (spinner) spinner.style.display = 'none'; - displayKCTestResults(data); - } - } catch (e) { - console.error('KC test WS parse error:', e); - } - }; - - ws.onclose = (ev) => { - setKcTestWs(null); - // Only show error if closed unexpectedly (not a normal close) - if (ev.code !== 1000 && ev.code !== 1001 && kcTestTargetId) { - const reason = ev.reason || t('kc.test.ws_closed'); - showToast(t('kc.test.error') + ': ' + reason, 'error'); - // Close lightbox on fatal errors (auth, bad target, etc.) - if (ev.code === 4001 || ev.code === 4003 || ev.code === 4004) { - if (typeof window.closeLightbox === 'function') window.closeLightbox(); - } - } - }; - - ws.onerror = () => { - // onclose will fire after onerror; no need to handle here - }; - - setKcTestWs(ws); - setKcTestTargetId(targetId); -} - -export async function testKCTarget(targetId: any) { - setKcTestTargetId(targetId); - - // Show lightbox immediately with a spinner - const lightbox = document.getElementById('image-lightbox')!; - const lbImg = document.getElementById('lightbox-image') as HTMLImageElement; - const statsEl = document.getElementById('lightbox-stats') as HTMLElement; - lbImg.style.display = 'none'; - lbImg.src = ''; - statsEl.style.display = 'none'; - - // Insert spinner if not already present - let spinner = lightbox.querySelector('.lightbox-spinner') as HTMLElement; - if (!spinner) { - spinner = document.createElement('div'); - spinner.className = 'lightbox-spinner loading-spinner'; - lightbox.querySelector('.lightbox-content')!.prepend(spinner); - } - spinner.style.display = ''; - - // Hide controls — KC test streams automatically - const refreshBtn = document.getElementById('lightbox-auto-refresh') as HTMLElement; - if (refreshBtn) refreshBtn.style.display = 'none'; - const fpsSelect = document.getElementById('lightbox-fps-select') as HTMLElement; - if (fpsSelect) fpsSelect.style.display = 'none'; - - lightbox.classList.add('active'); - lockBody(); - - // Use same FPS from CSS test settings and dynamic preview resolution - const fps = parseInt(localStorage.getItem('css_test_fps')!) || 15; - const previewWidth = Math.round(Math.min(window.innerWidth * 0.8, 1920) * Math.min(window.devicePixelRatio || 1, 2)); - _openKCTestWs(targetId, fps, previewWidth); -} - -export function stopKCTestAutoRefresh() { - if (kcTestAutoRefresh) { - clearInterval(kcTestAutoRefresh); - setKcTestAutoRefresh(null); - } - if (kcTestWs) { - try { kcTestWs.close(1000, 'lightbox closed'); } catch (_) {} - setKcTestWs(null); - } - setKcTestTargetId(null); -} - -export function displayKCTestResults(result: any) { - const srcImg = new window.Image(); - srcImg.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = srcImg.width; - canvas.height = srcImg.height; - const ctx = canvas.getContext('2d')!; - - // Draw captured frame - ctx.drawImage(srcImg, 0, 0); - - const w = srcImg.width; - const h = srcImg.height; - - // Draw each rectangle with extracted color overlay - result.rectangles.forEach((rect: any, i: number) => { - const px = rect.x * w; - const py = rect.y * h; - const pw = rect.width * w; - const ph = rect.height * h; - - const color = rect.color; - const borderColor = PATTERN_RECT_BORDERS[i % PATTERN_RECT_BORDERS.length]; - - // Semi-transparent fill with the extracted color - ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, 0.3)`; - ctx.fillRect(px, py, pw, ph); - - // Border using pattern colors for distinction - ctx.strokeStyle = borderColor; - ctx.lineWidth = 3; - ctx.strokeRect(px, py, pw, ph); - - // Color swatch in top-left corner of rect - const swatchSize = Math.max(16, Math.min(32, pw * 0.15)); - ctx.fillStyle = color.hex; - ctx.fillRect(px + 4, py + 4, swatchSize, swatchSize); - ctx.strokeStyle = '#fff'; - ctx.lineWidth = 1; - ctx.strokeRect(px + 4, py + 4, swatchSize, swatchSize); - - // Name label with shadow for readability - const fontSize = Math.max(12, Math.min(18, pw * 0.06)); - ctx.font = `bold ${fontSize}px sans-serif`; - const labelX = px + swatchSize + 10; - const labelY = py + 4 + swatchSize / 2 + fontSize / 3; - ctx.shadowColor = 'rgba(0,0,0,0.8)'; - ctx.shadowBlur = 4; - ctx.fillStyle = '#fff'; - ctx.fillText(rect.name, labelX, labelY); - - // Hex label below name - ctx.font = `${fontSize - 2}px monospace`; - ctx.fillText(color.hex, labelX, labelY + fontSize + 2); - ctx.shadowBlur = 0; - }); - - const dataUrl = canvas.toDataURL('image/jpeg', 0.92); - - // Build stats HTML - let statsHtml = `
`; - statsHtml += `${escapeHtml(result.pattern_template_name)} \u2022 ${escapeHtml(result.interpolation_mode)}`; - result.rectangles.forEach((rect: any) => { - const c = rect.color; - statsHtml += `
`; - statsHtml += `
`; - statsHtml += `${escapeHtml(rect.name)} ${c.hex}`; - statsHtml += `
`; - }); - statsHtml += `
`; - - // Hide spinner, show result in the already-open lightbox - const spinner = document.querySelector('.lightbox-spinner') as HTMLElement; - if (spinner) spinner.style.display = 'none'; - - const lbImg = document.getElementById('lightbox-image') as HTMLImageElement; - const statsEl = document.getElementById('lightbox-stats') as HTMLElement; - lbImg.src = dataUrl; - lbImg.style.display = ''; - statsEl.innerHTML = statsHtml; - statsEl.style.display = ''; - }; - srcImg.src = result.image; -} - -// ===== KEY COLORS EDITOR ===== - -function _autoGenerateKCName() { - if (_kcNameManuallyEdited) return; - if ((document.getElementById('kc-editor-id') as HTMLInputElement).value) return; - const sourceSelect = document.getElementById('kc-editor-source') as HTMLSelectElement; - const sourceName = sourceSelect.selectedOptions[0]?.dataset?.name || ''; - if (!sourceName) return; - const mode = (document.getElementById('kc-editor-interpolation') as HTMLSelectElement).value || 'average'; - const modeName = t(`kc.interpolation.${mode}`); - const patSelect = document.getElementById('kc-editor-pattern-template') as HTMLSelectElement; - const patName = patSelect.selectedOptions[0]?.dataset?.name || ''; - (document.getElementById('kc-editor-name') as HTMLInputElement).value = `${sourceName} \u00b7 ${patName} (${modeName})`; -} - -function _populateKCBrightnessVsDropdown(selectedId = '') { - const sel = document.getElementById('kc-editor-brightness-vs') as HTMLSelectElement; - // Keep the first "None" option, remove the rest - while (sel.options.length > 1) sel.remove(1); - _cachedValueSources.forEach((vs: any) => { - const opt = document.createElement('option'); - opt.value = vs.id; - opt.textContent = vs.name; - sel.appendChild(opt); - }); - sel.value = selectedId || ''; - _ensureBrightnessEntitySelect(); -} - -export async function showKCEditor(targetId: any = null, cloneData: any = null) { - try { - // Load sources, pattern templates, and value sources in parallel - const [sources, patTemplates, valueSources] = await Promise.all([ - streamsCache.fetch().catch((): any[] => []), - patternTemplatesCache.fetch().catch((): any[] => []), - valueSourcesCache.fetch(), - ]); - - // Populate source select - const sourceSelect = document.getElementById('kc-editor-source') as HTMLSelectElement; - sourceSelect.innerHTML = ''; - sources.forEach((s: any) => { - const opt = document.createElement('option'); - opt.value = s.id; - opt.dataset.name = s.name; - opt.textContent = s.name; - sourceSelect.appendChild(opt); - }); - - // Populate pattern template select - const patSelect = document.getElementById('kc-editor-pattern-template') as HTMLSelectElement; - patSelect.innerHTML = ''; - patTemplates.forEach((pt: any) => { - const opt = document.createElement('option'); - opt.value = pt.id; - opt.dataset.name = pt.name; - const rectCount = (pt.rectangles || []).length; - opt.textContent = `${pt.name} (${rectCount} rect${rectCount !== 1 ? 's' : ''})`; - patSelect.appendChild(opt); - }); - - // Set up visual selectors - _ensureColorModeIconSelect(); - _ensureSourceEntitySelect(sources); - _ensurePatternEntitySelect(patTemplates); - - let _editorTags: any[] = []; - if (targetId) { - const resp = await fetch(`${API_BASE}/output-targets/${targetId}`, { headers: getHeaders() }); - if (!resp.ok) throw new Error('Failed to load target'); - const target = await resp.json(); - _editorTags = target.tags || []; - const kcSettings = target.key_colors_settings || {}; - - (document.getElementById('kc-editor-id') as HTMLInputElement).value = target.id; - (document.getElementById('kc-editor-name') as HTMLInputElement).value = target.name; - sourceSelect.value = target.picture_source_id || ''; - (document.getElementById('kc-editor-fps') as HTMLInputElement).value = kcSettings.fps ?? 10; - (document.getElementById('kc-editor-fps-value') as HTMLElement).textContent = kcSettings.fps ?? 10; - (document.getElementById('kc-editor-interpolation') as HTMLSelectElement).value = kcSettings.interpolation_mode ?? 'average'; - if (_kcColorModeIconSelect) _kcColorModeIconSelect.setValue(kcSettings.interpolation_mode ?? 'average'); - (document.getElementById('kc-editor-smoothing') as HTMLInputElement).value = kcSettings.smoothing ?? 0.3; - (document.getElementById('kc-editor-smoothing-value') as HTMLElement).textContent = kcSettings.smoothing ?? 0.3; - patSelect.value = kcSettings.pattern_template_id || ''; - _populateKCBrightnessVsDropdown(kcSettings.brightness_value_source_id || ''); - (document.getElementById('kc-editor-title') as HTMLElement).innerHTML = `${ICON_PALETTE} ${t('kc.edit')}`; - } else if (cloneData) { - _editorTags = cloneData.tags || []; - const kcSettings = cloneData.key_colors_settings || {}; - (document.getElementById('kc-editor-id') as HTMLInputElement).value = ''; - (document.getElementById('kc-editor-name') as HTMLInputElement).value = (cloneData.name || '') + ' (Copy)'; - sourceSelect.value = cloneData.picture_source_id || ''; - (document.getElementById('kc-editor-fps') as HTMLInputElement).value = kcSettings.fps ?? 10; - (document.getElementById('kc-editor-fps-value') as HTMLElement).textContent = kcSettings.fps ?? 10; - (document.getElementById('kc-editor-interpolation') as HTMLSelectElement).value = kcSettings.interpolation_mode ?? 'average'; - if (_kcColorModeIconSelect) _kcColorModeIconSelect.setValue(kcSettings.interpolation_mode ?? 'average'); - (document.getElementById('kc-editor-smoothing') as HTMLInputElement).value = kcSettings.smoothing ?? 0.3; - (document.getElementById('kc-editor-smoothing-value') as HTMLElement).textContent = kcSettings.smoothing ?? 0.3; - patSelect.value = kcSettings.pattern_template_id || ''; - _populateKCBrightnessVsDropdown(kcSettings.brightness_value_source_id || ''); - (document.getElementById('kc-editor-title') as HTMLElement).innerHTML = `${ICON_PALETTE} ${t('kc.add')}`; - } else { - (document.getElementById('kc-editor-id') as HTMLInputElement).value = ''; - (document.getElementById('kc-editor-name') as HTMLInputElement).value = ''; - if (sourceSelect.options.length > 0) sourceSelect.selectedIndex = 0; - (document.getElementById('kc-editor-fps') as HTMLInputElement).value = '10' as any; - (document.getElementById('kc-editor-fps-value') as HTMLElement).textContent = '10'; - (document.getElementById('kc-editor-interpolation') as HTMLSelectElement).value = 'average'; - if (_kcColorModeIconSelect) _kcColorModeIconSelect.setValue('average'); - (document.getElementById('kc-editor-smoothing') as HTMLInputElement).value = '0.3' as any; - (document.getElementById('kc-editor-smoothing-value') as HTMLElement).textContent = '0.3'; - if (patTemplates.length > 0) patSelect.value = patTemplates[0].id; - _populateKCBrightnessVsDropdown(''); - (document.getElementById('kc-editor-title') as HTMLElement).innerHTML = `${ICON_PALETTE} ${t('kc.add')}`; - } - - // Auto-name - set_kcNameManuallyEdited(!!(targetId || cloneData)); - (document.getElementById('kc-editor-name') as HTMLInputElement).oninput = () => { set_kcNameManuallyEdited(true); }; - sourceSelect.onchange = () => _autoGenerateKCName(); - (document.getElementById('kc-editor-interpolation') as HTMLSelectElement).onchange = () => _autoGenerateKCName(); - patSelect.onchange = () => _autoGenerateKCName(); - if (!targetId && !cloneData) _autoGenerateKCName(); - - // Tags - if (_kcTagsInput) _kcTagsInput.destroy(); - _kcTagsInput = new TagInput(document.getElementById('kc-tags-container'), { - placeholder: t('tags.placeholder'), - }); - _kcTagsInput.setValue(_editorTags); - - kcEditorModal.snapshot(); - kcEditorModal.open(); - - (document.getElementById('kc-editor-error') as HTMLElement).style.display = 'none'; - setTimeout(() => desktopFocus(document.getElementById('kc-editor-name')), 100); - } catch (error) { - console.error('Failed to open KC editor:', error); - showToast(t('kc_target.error.editor_open_failed'), 'error'); - } -} - -export function isKCEditorDirty() { - return kcEditorModal.isDirty(); -} - -export async function closeKCEditorModal() { - await kcEditorModal.close(); - set_kcNameManuallyEdited(false); -} - -export function forceCloseKCEditorModal() { - if (_kcTagsInput) { _kcTagsInput.destroy(); _kcTagsInput = null; } - kcEditorModal.forceClose(); - set_kcNameManuallyEdited(false); -} - -export async function saveKCEditor() { - const targetId = (document.getElementById('kc-editor-id') as HTMLInputElement).value; - const name = (document.getElementById('kc-editor-name') as HTMLInputElement).value.trim(); - const sourceId = (document.getElementById('kc-editor-source') as HTMLSelectElement).value; - const fps = parseInt((document.getElementById('kc-editor-fps') as HTMLInputElement).value) || 10; - const interpolation = (document.getElementById('kc-editor-interpolation') as HTMLSelectElement).value; - const smoothing = parseFloat((document.getElementById('kc-editor-smoothing') as HTMLInputElement).value); - const patternTemplateId = (document.getElementById('kc-editor-pattern-template') as HTMLSelectElement).value; - const brightnessVsId = (document.getElementById('kc-editor-brightness-vs') as HTMLSelectElement).value; - - if (!name) { - kcEditorModal.showError(t('kc.error.required')); - return; - } - - if (!patternTemplateId) { - kcEditorModal.showError(t('kc.error.no_pattern')); - return; - } - - const payload: any = { - name, - picture_source_id: sourceId, - tags: _kcTagsInput ? _kcTagsInput.getValue() : [], - key_colors_settings: { - fps, - interpolation_mode: interpolation, - smoothing, - pattern_template_id: patternTemplateId, - brightness_value_source_id: brightnessVsId, - }, - }; - - try { - let response; - if (targetId) { - response = await fetchWithAuth(`/output-targets/${targetId}`, { - method: 'PUT', - body: JSON.stringify(payload), - }); - } else { - payload.target_type = 'key_colors'; - response = await fetchWithAuth('/output-targets', { - method: 'POST', - body: JSON.stringify(payload), - }); - } - - if (!response.ok) { - const err = await response.json(); - throw new Error(err.detail || 'Failed to save'); - } - - showToast(targetId ? t('kc.updated') : t('kc.created'), 'success'); - outputTargetsCache.invalidate(); - kcEditorModal.forceClose(); - // Use window.* to avoid circular import with targets.js - if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab(); - } catch (error: any) { - if (error.isAuth) return; - console.error('Error saving KC target:', error); - kcEditorModal.showError(error.message); - } -} - -export async function cloneKCTarget(targetId: any) { - try { - const targets = await outputTargetsCache.fetch(); - const target = targets.find((t: any) => t.id === targetId); - if (!target) throw new Error('Target not found'); - showKCEditor(null, target); - } catch (error: any) { - if (error.isAuth) return; - showToast(t('kc_target.error.clone_failed'), 'error'); - } -} - -export async function deleteKCTarget(targetId: any) { - const confirmed = await showConfirm(t('kc.delete.confirm')); - if (!confirmed) return; - - try { - disconnectKCWebSocket(targetId); - const response = await fetchWithAuth(`/output-targets/${targetId}`, { - method: 'DELETE', - }); - if (response.ok) { - showToast(t('kc.deleted'), 'success'); - outputTargetsCache.invalidate(); - // Use window.* to avoid circular import with targets.js - if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab(); - } else { - const error = await response.json(); - showToast(error.detail || t('kc_target.error.delete_failed'), 'error'); - } - } catch (error: any) { - if (error.isAuth) return; - showToast(t('kc_target.error.delete_failed'), 'error'); - } -} - -// ===== KC BRIGHTNESS ===== - -export function updateKCBrightnessLabel(targetId: any, value: any) { - const slider = document.querySelector(`[data-kc-brightness="${CSS.escape(targetId)}"]`) as HTMLElement; - if (slider) slider.title = Math.round(parseInt(value) / 255 * 100) + '%'; -} - -export async function saveKCBrightness(targetId: any, value: any) { - const brightness = parseInt(value) / 255; - try { - await fetch(`${API_BASE}/output-targets/${targetId}`, { - method: 'PUT', - headers: getHeaders(), - body: JSON.stringify({ key_colors_settings: { brightness } }), - }); - } catch (err) { - console.error('Failed to save KC brightness:', err); - showToast(t('kc.error.brightness') || 'Failed to save brightness', 'error'); - } -} - -// ===== KEY COLORS WEBSOCKET ===== - -export function connectKCWebSocket(targetId: any) { - // Disconnect existing connection if any - disconnectKCWebSocket(targetId); - - const key = localStorage.getItem('wled_api_key'); - if (!key) return; - - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}${API_BASE}/output-targets/${targetId}/ws?token=${encodeURIComponent(key)}`; - - try { - const ws = new WebSocket(wsUrl); - - ws.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - updateKCColorSwatches(targetId, data.colors || {}); - } catch (e) { - console.error('Failed to parse KC WebSocket message:', e); - } - }; - - ws.onclose = () => { - delete kcWebSockets[targetId]; - }; - - ws.onerror = (error) => { - console.error(`KC WebSocket error for ${targetId}:`, error); - }; - - kcWebSockets[targetId] = ws; - } catch (error) { - console.error(`Failed to connect KC WebSocket for ${targetId}:`, error); - } -} - -export function disconnectKCWebSocket(targetId: any) { - const ws = kcWebSockets[targetId]; - if (ws) { - ws.close(); - delete kcWebSockets[targetId]; - } -} - -export function disconnectAllKCWebSockets() { - Object.keys(kcWebSockets).forEach(targetId => disconnectKCWebSocket(targetId)); -} - -export function updateKCColorSwatches(targetId: any, colors: any) { - const container = document.getElementById(`kc-swatches-${targetId}`); - if (!container) return; - - const entries = Object.entries(colors); - if (entries.length === 0) { - container.innerHTML = `${t('kc.colors.none')}`; - return; - } - - container.innerHTML = entries.map(([name, color]: [string, any]) => { - const hex = color.hex || `#${(color.r || 0).toString(16).padStart(2, '0')}${(color.g || 0).toString(16).padStart(2, '0')}${(color.b || 0).toString(16).padStart(2, '0')}`; - return ` -
-
- ${escapeHtml(name)} -
- `; - }).join(''); -} diff --git a/server/src/wled_controller/static/js/features/targets.ts b/server/src/wled_controller/static/js/features/targets.ts index 4b77e99..f73df99 100644 --- a/server/src/wled_controller/static/js/features/targets.ts +++ b/server/src/wled_controller/static/js/features/targets.ts @@ -18,7 +18,7 @@ import { showToast, showConfirm, formatUptime, formatCompact, setTabRefreshing, import { Modal } from '../core/modal.ts'; import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness, enrichOpenrgbZoneBadges, _computeMaxFps, getZoneCountCache, formatRelativeTime } from './devices.ts'; import { _splitOpenrgbZone } from './device-discovery.ts'; -import { createHALightTargetCard, initHALightTargetDelegation, patchHALightTargetMetrics } from './ha-light-targets.ts'; +import { createHALightTargetCard, initHALightTargetDelegation, patchHALightTargetMetrics, connectHALightWS, disconnectHALightWS } from './ha-light-targets.ts'; import { getValueSourceIcon, getTargetTypeIcon, getDeviceTypeIcon, getColorStripIcon, ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP, @@ -616,21 +616,12 @@ export async function loadTargetsTab() { const devicesWithState = devices.map(d => ({ ...d, state: allDeviceStates[d.id] || {} })); - // Enrich targets with state/metrics; fetch colors only for running KC targets - const targetsWithState = await Promise.all( - targets.map(async (target) => { - const state = allTargetStates[target.id] || {}; - const metrics = allTargetMetrics[target.id] || {}; - let latestColors = null; - if (target.target_type === 'key_colors' && state.processing) { - try { - const colorsResp = await fetch(`${API_BASE}/output-targets/${target.id}/colors`, { headers: getHeaders() }); - if (colorsResp.ok) latestColors = await colorsResp.json(); - } catch {} - } - return { ...target, state, metrics, latestColors }; - }) - ); + // Enrich targets with state/metrics + const targetsWithState = targets.map((target) => { + const state = allTargetStates[target.id] || {}; + const metrics = allTargetMetrics[target.id] || {}; + return { ...target, state, metrics }; + }); // Build device map for target name resolution const deviceMap = {}; @@ -769,6 +760,15 @@ export async function loadTargetsTab() { if (!processingLedIds.has(id)) disconnectLedPreviewWS(id); }); + // Manage HA light color preview WebSockets + haLightTargets.forEach(target => { + if (target.state && target.state.processing) { + connectHALightWS(target.id); + } else { + disconnectHALightWS(target.id); + } + }); + // FPS charts: only destroy charts for replaced/removed cards (or all on first render) if (changedTargetIds) { // Incremental: destroy only charts whose cards were replaced or removed diff --git a/server/src/wled_controller/static/js/global.d.ts b/server/src/wled_controller/static/js/global.d.ts index 41e7e5a..2947895 100644 --- a/server/src/wled_controller/static/js/global.d.ts +++ b/server/src/wled_controller/static/js/global.d.ts @@ -157,19 +157,6 @@ interface Window { closeTestAudioTemplateModal: (...args: any[]) => any; startAudioTemplateTest: (...args: any[]) => any; - // ─── KC Targets ─── - createKCTargetCard: (...args: any[]) => any; - testKCTarget: (...args: any[]) => any; - showKCEditor: (...args: any[]) => any; - closeKCEditorModal: (...args: any[]) => any; - forceCloseKCEditorModal: (...args: any[]) => any; - saveKCEditor: (...args: any[]) => any; - deleteKCTarget: (...args: any[]) => any; - disconnectAllKCWebSockets: (...args: any[]) => any; - updateKCBrightnessLabel: (...args: any[]) => any; - saveKCBrightness: (...args: any[]) => any; - cloneKCTarget: (...args: any[]) => any; - // ─── Pattern Templates ─── createPatternTemplateCard: (...args: any[]) => any; showPatternTemplateEditor: (...args: any[]) => any; @@ -226,8 +213,7 @@ interface Window { startTargetProcessing: (...args: any[]) => any; stopTargetProcessing: (...args: any[]) => any; stopAllLedTargets: (...args: any[]) => any; - stopAllKCTargets: (...args: any[]) => any; - startTargetOverlay: (...args: any[]) => any; +startTargetOverlay: (...args: any[]) => any; stopTargetOverlay: (...args: any[]) => any; deleteTarget: (...args: any[]) => any; cloneTarget: (...args: any[]) => any; diff --git a/server/src/wled_controller/static/js/types.ts b/server/src/wled_controller/static/js/types.ts index eab5751..862a8ac 100644 --- a/server/src/wled_controller/static/js/types.ts +++ b/server/src/wled_controller/static/js/types.ts @@ -45,16 +45,7 @@ export interface Device { // ── Output Target ───────────────────────────────────────────── -export type TargetType = 'led' | 'key_colors'; - -export interface KeyColorsSettings { - fps: number; - interpolation_mode: string; - smoothing: number; - pattern_template_id: string; - brightness: number; - brightness_value_source_id: string; -} +export type TargetType = 'led' | 'ha_light'; export interface OutputTarget { id: string; @@ -76,9 +67,19 @@ export interface OutputTarget { adaptive_fps?: boolean; protocol?: string; - // Key Colors target fields - picture_source_id?: string; - key_colors_settings?: KeyColorsSettings; + // HA light target fields + ha_source_id?: string; + ha_light_mappings?: HALightMapping[]; + update_rate?: number; + ha_transition?: number; + color_tolerance?: number; +} + +export interface HALightMapping { + entity_id: string; + led_start: number; + led_end: number; + brightness_scale: number; } // ── Color Strip Source ──────────────────────────────────────── diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index ff5b8ab..d3a90ac 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -1,2109 +1,2066 @@ { - "app.title": "LED Grab", - "app.version": "Version:", - "app.api_docs": "API Documentation", - "app.connection_lost": "Server unreachable", - "app.connection_retrying": "Attempting to reconnect…", - "app.server_restarting": "Server restarting…", - "app.server_restarting_sub": "Please wait, the server will be back shortly.", - "demo.badge": "DEMO", - "demo.banner": "You're in demo mode — all devices and data are virtual. No real hardware is used.", - "theme.toggle": "Toggle theme", - "bg.anim.toggle": "Toggle ambient background", - "accent.title": "Accent color", - "accent.custom": "Custom", - "accent.reset": "Reset", - "locale.change": "Change language", - "auth.login": "Login", - "auth.logout": "Logout", - "auth.authenticated": "● Authenticated", - "auth.title": "Login to LED Grab", - "auth.message": "Please enter your API key to authenticate and access the LED Grab.", - "auth.label": "API Key:", - "auth.placeholder": "Enter your API key...", - "auth.hint": "Your API key will be stored securely in your browser's local storage.", - "auth.button.cancel": "Cancel", - "auth.button.login": "Login", - "auth.error.required": "Please enter an API key", - "auth.success": "Logged in successfully!", - "auth.logout.confirm": "Are you sure you want to logout?", - "auth.logout.success": "Logged out successfully", - "auth.please_login": "Please login to view", - "auth.session_expired": "Your session has expired or the API key is invalid. Please login again.", - "auth.prompt_update": "Current API key is set. Enter new key to update or leave blank to remove:", - "auth.prompt_enter": "Enter your API key:", - "auth.toggle_password": "Toggle password visibility", - "api_key.login": "Login", - "displays.title": "Available Displays", - "displays.layout": "Displays", - "displays.information": "Display Information", - "displays.legend.primary": "Primary Display", - "displays.legend.secondary": "Secondary Display", - "displays.badge.primary": "Primary", - "displays.badge.secondary": "Secondary", - "displays.resolution": "Resolution:", - "displays.refresh_rate": "Refresh Rate:", - "displays.position": "Position:", - "displays.index": "Display Index:", - "displays.loading": "Loading displays...", - "displays.none": "No displays available", - "displays.failed": "Failed to load displays", - "displays.picker.title": "Select a Display", - "displays.picker.title.device": "Select a Device", - "displays.picker.select": "Select display...", - "displays.picker.click_to_select": "Click to select this display", - "displays.picker.adb_connect": "Connect ADB device", - "displays.picker.adb_connect.placeholder": "IP address (e.g. 192.168.2.201)", - "displays.picker.adb_connect.button": "Connect", - "displays.picker.adb_connect.success": "Device connected", - "displays.picker.adb_connect.error": "Failed to connect device", - "displays.picker.adb_disconnect": "Disconnect", - "displays.picker.no_android": "No Android devices found. Connect via USB or enter IP above.", - "templates.title": "Engine Templates", - "templates.description": "Capture templates define how the screen is captured. Each template uses a specific capture engine (MSS, DXcam, WGC) with custom settings. Assign templates to devices for optimal performance.", - "templates.loading": "Loading templates...", - "templates.empty": "No capture templates configured", - "templates.add": "Add Engine Template", - "templates.edit": "Edit Engine Template", - "templates.name": "Template Name:", - "templates.name.placeholder": "My Custom Template", - "templates.description.label": "Description (optional):", - "templates.description.placeholder": "Describe this template...", - "templates.engine": "Capture Engine:", - "templates.engine.hint": "Select the screen capture technology to use", - "templates.engine.select": "Select an engine...", - "templates.engine.unavailable": "Unavailable", - "templates.engine.unavailable.hint": "This engine is not available on your system", - "templates.engine.mss.desc": "Cross-platform, pure Python", - "templates.engine.dxcam.desc": "DirectX, low latency", - "templates.engine.bettercam.desc": "DirectX, high performance", - "templates.engine.camera.desc": "USB/IP camera capture", - "templates.engine.scrcpy.desc": "Android screen mirror", - "templates.engine.wgc.desc": "Windows Graphics Capture", - "templates.config": "Configuration", - "templates.config.show": "Show configuration", - "templates.config.none": "No additional configuration", - "templates.config.default": "Default", - "templates.config.camera_backend.auto": "Auto-detect best backend", - "templates.config.camera_backend.dshow": "Windows DirectShow", - "templates.config.camera_backend.msmf": "Windows Media Foundation", - "templates.config.camera_backend.v4l2": "Linux Video4Linux2", - "templates.created": "Template created successfully", - "templates.updated": "Template updated successfully", - "templates.deleted": "Template deleted successfully", - "templates.delete.confirm": "Are you sure you want to delete this template?", - "templates.error.load": "Failed to load templates", - "templates.error.engines": "Failed to load engines", - "templates.error.required": "Please fill in all required fields", - "templates.error.delete": "Failed to delete template", - "templates.test.title": "Test Capture", - "templates.test.description": "Test this template before saving to see a capture preview and performance metrics.", - "templates.test.display": "Display:", - "templates.test.display.select": "Select display...", - "templates.test.duration": "Capture Duration (s):", - "templates.test.border_width": "Border Width (px):", - "templates.test.run": "Run", - "templates.test.running": "Running test...", - "templates.test.results.preview": "Full Capture Preview", - "templates.test.results.borders": "Border Extraction", - "templates.test.results.top": "Top", - "templates.test.results.right": "Right", - "templates.test.results.bottom": "Bottom", - "templates.test.results.left": "Left", - "templates.test.results.performance": "Performance", - "templates.test.results.capture_time": "Capture", - "templates.test.results.extraction_time": "Extraction", - "templates.test.results.total_time": "Total", - "templates.test.results.max_fps": "Max FPS", - "templates.test.results.duration": "Duration", - "templates.test.results.frame_count": "Frames", - "templates.test.results.actual_fps": "Actual FPS", - "templates.test.results.avg_capture_time": "Avg Capture", - "templates.test.results.resolution": "Resolution:", - "templates.test.error.no_engine": "Please select a capture engine", - "templates.test.error.no_display": "Please select a display", - "templates.test.error.failed": "Test failed", - "devices.title": "Devices", - "device.select_type": "Select Device Type", - "devices.add": "Add New Device", - "devices.loading": "Loading devices...", - "devices.none": "No devices configured", - "devices.failed": "Failed to load devices", - "devices.wled_config": "WLED Configuration:", - "devices.wled_note": "Configure your WLED device (effects, segments, color order, power limits, etc.) using the", - "devices.wled_link": "official WLED app", - "devices.wled_note_or": "or the built-in", - "devices.wled_webui_link": "WLED Web UI", - "devices.wled_note_webui": "(open your device's IP in a browser).", - "devices.wled_note2": "This controller sends pixel color data and controls brightness per device.", - "device.scan": "Auto Discovery", - "device.scan.empty": "No devices found", - "device.scan.error": "Network scan failed", - "device.scan.already_added": "Already added", - "device.scan.selected": "Device selected", - "device.type": "Device Type:", - "device.type.hint": "Select the type of LED controller", - "device.type.wled": "WLED", - "device.type.wled.desc": "WiFi LED controller over HTTP/UDP", - "device.type.adalight": "Adalight", - "device.type.adalight.desc": "Serial LED protocol for Arduino", - "device.type.ambiled": "AmbiLED", - "device.type.ambiled.desc": "Serial protocol for AmbiLED devices", - "device.type.mqtt": "MQTT", - "device.type.mqtt.desc": "Publish LED data via MQTT broker", - "device.type.ws": "WebSocket", - "device.type.ws.desc": "Stream LED data to WebSocket clients", - "device.type.openrgb": "OpenRGB", - "device.type.openrgb.desc": "Control RGB peripherals via OpenRGB", - "device.type.dmx": "DMX", - "device.type.dmx.desc": "Art-Net / sACN (E1.31) stage lighting", - "device.type.mock": "Mock", - "device.type.mock.desc": "Virtual device for testing", - "device.type.espnow": "ESP-NOW", - "device.type.espnow.desc": "Ultra-low-latency via ESP32 gateway", - "device.type.hue": "Philips Hue", - "device.type.hue.desc": "Hue Entertainment API streaming", - "device.type.usbhid": "USB HID", - "device.type.usbhid.desc": "USB RGB peripherals (keyboards, mice)", - "device.type.spi": "SPI Direct", - "device.type.spi.desc": "Raspberry Pi GPIO/SPI LED strips", - "device.type.chroma": "Razer Chroma", - "device.type.chroma.desc": "Razer peripherals via Chroma SDK", - "device.type.gamesense": "SteelSeries", - "device.type.gamesense.desc": "SteelSeries peripherals via GameSense", - "device.chroma.device_type": "Peripheral Type:", - "device.chroma.device_type.hint": "Which Razer peripheral to control via Chroma SDK", - "device.gamesense.device_type": "Peripheral Type:", - "device.gamesense.device_type.hint": "Which SteelSeries peripheral to control via GameSense", - "device.espnow.peer_mac": "Peer MAC:", - "device.espnow.peer_mac.hint": "MAC address of the remote ESP32 receiver (e.g. AA:BB:CC:DD:EE:FF)", - "device.espnow.channel": "WiFi Channel:", - "device.espnow.channel.hint": "WiFi channel (1-14). Must match the receiver's channel.", - "device.hue.url": "Bridge IP:", - "device.hue.url.hint": "IP address of your Hue bridge", - "device.hue.username": "Bridge Username:", - "device.hue.username.hint": "Hue bridge application key from pairing", - "device.hue.client_key": "Client Key:", - "device.hue.client_key.hint": "Entertainment API client key (hex string from pairing)", - "device.hue.group_id": "Entertainment Group:", - "device.hue.group_id.hint": "Entertainment configuration ID from your Hue bridge", - "device.usbhid.url": "VID:PID:", - "device.usbhid.url.hint": "USB Vendor:Product ID in hex (e.g. 1532:0084)", - "device.spi.url": "GPIO/SPI Path:", - "device.spi.url.hint": "GPIO pin or SPI device path (e.g. spi://gpio:18)", - "device.spi.speed": "SPI Speed (Hz):", - "device.spi.speed.hint": "SPI clock speed. 800000 Hz for WS2812, 2400000 Hz for APA102.", - "device.spi.led_type": "LED Chipset:", - "device.spi.led_type.hint": "Type of addressable LED strip connected to the GPIO/SPI pin", - "device.spi.led_type.ws2812b.desc": "Most common, 800 KHz data, 3-wire RGB", - "device.spi.led_type.ws2812.desc": "Original WS2812, 800 KHz, 3-wire RGB", - "device.spi.led_type.ws2811.desc": "External driver IC, 400 KHz, 12V strips", - "device.spi.led_type.sk6812.desc": "Samsung LED, 800 KHz, 3-wire RGB", - "device.spi.led_type.sk6812_rgbw.desc": "SK6812 with dedicated white channel", - "device.gamesense.peripheral.keyboard": "Keyboard", - "device.gamesense.peripheral.keyboard.desc": "Per-key RGB illumination", - "device.gamesense.peripheral.mouse": "Mouse", - "device.gamesense.peripheral.mouse.desc": "Mouse RGB zones", - "device.gamesense.peripheral.headset": "Headset", - "device.gamesense.peripheral.headset.desc": "Headset earcup lighting", - "device.gamesense.peripheral.mousepad": "Mousepad", - "device.gamesense.peripheral.mousepad.desc": "Mousepad edge lighting zones", - "device.gamesense.peripheral.indicator": "Indicator", - "device.gamesense.peripheral.indicator.desc": "OLED/LED status indicator", - "device.css_processing_template": "Strip Processing Template:", - "device.css_processing_template.hint": "Default processing template applied to all color strip outputs on this device", - "device.dmx_protocol": "DMX Protocol:", - "device.dmx_protocol.hint": "Art-Net uses UDP port 6454, sACN (E1.31) uses UDP port 5568", - "device.dmx_protocol.artnet.desc": "UDP unicast, port 6454", - "device.dmx_protocol.sacn.desc": "Multicast/unicast, port 5568", - "device.dmx_start_universe": "Start Universe:", - "device.dmx_start_universe.hint": "First DMX universe (0-32767). Multiple universes are used automatically for >170 LEDs.", - "device.dmx_start_channel": "Start Channel:", - "device.dmx_start_channel.hint": "First DMX channel within the universe (1-512)", - "device.dmx.url": "IP Address:", - "device.dmx.url.hint": "IP address of the DMX node (e.g. 192.168.1.50)", - "device.dmx.url.placeholder": "192.168.1.50", - "device.serial_port": "Serial Port:", - "device.serial_port.hint": "Select the COM port of the Adalight device", - "device.serial_port.none": "No serial ports found", - "device.serial_port.select": "Select a port...", - "device.led_count_manual.hint": "Number of LEDs on the strip (must match your Arduino sketch)", - "device.baud_rate": "Baud Rate:", - "device.baud_rate.hint": "Serial communication speed. Higher = more FPS but requires matching Arduino sketch.", - "device.led_type": "LED Type:", - "device.led_type.hint": "RGB (3 channels) or RGBW (4 channels with dedicated white)", - "device.send_latency": "Send Latency (ms):", - "device.send_latency.hint": "Simulated network/serial delay per frame in milliseconds", - "device.mqtt_topic": "MQTT Topic:", - "device.mqtt_topic.hint": "MQTT topic path for publishing pixel data (e.g. mqtt://ledgrab/device/name)", - "device.mqtt_topic.placeholder": "mqtt://ledgrab/device/living-room", - "device.ws_url": "Connection URL:", - "device.ws_url.hint": "WebSocket URL for clients to connect and receive LED data", - "device.openrgb.url": "OpenRGB URL:", - "device.openrgb.url.hint": "OpenRGB server address (e.g. openrgb://localhost:6742/0)", - "device.openrgb.zone": "Zones:", - "device.openrgb.zone.hint": "Select which LED zones to control (leave all unchecked for all zones)", - "device.openrgb.zone.loading": "Loading zones…", - "device.openrgb.zone.error": "Failed to load zones", - "device.openrgb.mode": "Zone mode:", - "device.openrgb.mode.hint": "Combined treats all zones as one continuous LED strip. Separate renders each zone independently with the full effect.", - "device.openrgb.mode.combined": "Combined strip", - "device.openrgb.mode.separate": "Independent zones", - "device.openrgb.added_multiple": "Added {count} devices", - "device.url.hint": "IP address or hostname of the device (e.g. http://192.168.1.100)", - "device.name": "Device Name:", - "device.name.placeholder": "Living Room TV", - "device.url": "URL:", - "device.url.placeholder": "http://192.168.1.100", - "device.led_count": "LED Count:", - "device.led_count.hint": "Number of LEDs configured in the device", - "device.led_count.hint.auto": "Auto-detected from device", - "device.button.add": "Add Device", - "device.button.start": "Start", - "device.button.stop": "Stop", - "device.button.settings": "General Settings", - "device.button.capture_settings": "Capture Settings", - "device.button.calibrate": "Calibrate", - "device.button.remove": "Remove", - "device.button.webui": "Open Device Web UI", - "device.button.power_off": "Turn Off", - "device.button.ping": "Ping Device", - "device.ping.online": "Online ({ms}ms)", - "device.ping.offline": "Device offline", - "device.ping.error": "Ping failed", - "device.power.off_success": "Device turned off", - "device.status.connected": "Connected", - "device.status.disconnected": "Disconnected", - "device.status.error": "Error", - "device.status.processing": "Processing", - "device.status.idle": "Idle", - "device.fps": "FPS:", - "device.display": "Display:", - "device.remove.confirm": "Are you sure you want to remove this device?", - "device.added": "Device added successfully", - "device.removed": "Device removed", - "device.started": "Processing started", - "device.stopped": "Processing stopped", - "device.metrics.actual_fps": "Actual FPS", - "device.metrics.current_fps": "Current FPS", - "device.metrics.target_fps": "Target FPS", - "device.metrics.potential_fps": "Potential FPS", - "device.metrics.frames": "Frames", - "device.metrics.frames_skipped": "Skipped", - "device.metrics.keepalive": "Keepalive", - "device.metrics.errors": "Errors", - "device.metrics.uptime": "Uptime", - "device.metrics.timing": "Pipeline timing:", - "device.metrics.device_fps": "Device refresh rate", - "device.health.online": "Online", - "device.health.offline": "Offline", - "device.health.streaming_unreachable": "Unreachable during streaming", - "device.health.checking": "Checking...", - "device.last_seen.label": "Last seen", - "device.last_seen.just_now": "just now", - "device.last_seen.seconds": "%ds ago", - "device.last_seen.minutes": "%dm ago", - "device.last_seen.hours": "%dh ago", - "device.last_seen.days": "%dd ago", - "device.tutorial.start": "Start tutorial", - "device.tip.metadata": "Device info (LED count, type, color channels) is auto-detected from the device", - "device.tip.brightness": "Slide to adjust device brightness", - "device.tip.start": "Start or stop screen capture processing", - "device.tip.settings": "Configure general device settings (name, URL, health check)", - "device.tip.capture_settings": "Configure capture settings (display, capture template)", - "device.tip.calibrate": "Calibrate LED positions, direction, and coverage", - "device.tip.webui": "Open the device's built-in web interface for advanced configuration", - "device.tip.add": "Click here to add a new LED device", - "settings.title": "Settings", - "settings.tab.general": "General", - "settings.tab.backup": "Backup", - "settings.tab.mqtt": "MQTT", - "settings.tab.appearance": "Appearance", - "settings.logs.open_viewer": "Open Log Viewer", - "settings.external_url.label": "External URL", - "settings.external_url.hint": "If set, this base URL is used in webhook URLs and other user-visible links instead of the auto-detected local IP. Example: https://myserver.example.com:8080", - "settings.external_url.placeholder": "https://myserver.example.com:8080", - "settings.external_url.save": "Save", - "settings.external_url.saved": "External URL saved", - "settings.external_url.save_error": "Failed to save external URL", - "settings.general.title": "General Settings", - "settings.capture.title": "Capture Settings", - "settings.capture.saved": "Capture settings updated", - "settings.capture.failed": "Failed to save capture settings", - "settings.brightness": "Brightness:", - "settings.brightness.hint": "Global brightness for this device (0-100%)", - "settings.url.hint": "IP address or hostname of the device", - "settings.display_index": "Display:", - "settings.display_index.hint": "Which screen to capture for this device", - "settings.fps": "Target FPS:", - "settings.fps.hint": "Target frames per second (10-90)", - "settings.capture_template": "Engine Template:", - "settings.capture_template.hint": "Screen capture engine and configuration for this device", - "settings.button.cancel": "Cancel", - "settings.health_interval": "Health Check Interval (s):", - "settings.health_interval.hint": "How often to check the device status (5-600 seconds)", - "settings.auto_shutdown": "Auto Restore:", - "settings.auto_shutdown.hint": "Restore device to idle state when targets stop or server shuts down", - "settings.button.save": "Save Changes", - "settings.saved": "Settings saved successfully", - "settings.failed": "Failed to save settings", - "calibration.title": "LED Calibration", - "calibration.tip.led_count": "Enter LED count per edge", - "calibration.tip.start_corner": "Click a corner to set the start position", - "calibration.tip.direction": "Toggle LED strip direction (clockwise / counterclockwise)", - "calibration.tip.offset": "Set LED offset — distance from LED 0 to the start corner", - "calibration.tip.span": "Drag green bars to adjust coverage span", - "calibration.tip.test": "Click an edge to toggle test LEDs", - "calibration.tip.overlay": "Toggle screen overlay to see LED positions and numbering on your monitor", - "calibration.tip.toggle_inputs": "Click total LED count to toggle edge inputs", - "calibration.tip.border_width": "How many pixels from the screen edge to sample for LED colors", - "calibration.tip.skip_leds_start": "Skip LEDs at the start of the strip — skipped LEDs stay off", - "calibration.tip.skip_leds_end": "Skip LEDs at the end of the strip — skipped LEDs stay off", - "tour.welcome": "Welcome to LED Grab! This quick tour will show you around the interface. Use arrow keys or buttons to navigate.", - "tour.dashboard": "Dashboard — live overview of running targets, automations, and device health at a glance.", - "tour.targets": "Targets — add WLED devices, configure LED targets with capture settings and calibration.", - "tour.sources": "Sources — manage capture templates, picture sources, audio sources, and color strips.", - "tour.graph": "Graph — visual overview of all entities and their connections. Drag ports to connect, right-click edges to disconnect.", - "tour.automations": "Automations — automate scene switching with time, audio, or value conditions.", - "tour.settings": "Settings — backup and restore configuration, manage auto-backups.", - "tour.api": "API Docs — interactive REST API documentation powered by Swagger.", - "tour.search": "Search — quickly find and navigate to any entity with Ctrl+K.", - "tour.theme": "Theme — switch between dark and light mode.", - "tour.accent": "Accent color — customize the UI accent color to your preference.", - "tour.language": "Language — choose your preferred interface language.", - "tour.restart": "Restart tutorial", - "tour.dash.perf": "Performance — real-time FPS charts, latency metrics, and poll interval control.", - "tour.dash.running": "Running targets — live streaming metrics and quick stop control.", - "tour.dash.stopped": "Stopped targets — ready to start with one click.", - "tour.dash.automations": "Automations — active automation status and quick enable/disable toggle.", - "tour.tgt.led_tab": "LED tab — standard LED strip targets with device and color strip configuration.", - "tour.tgt.devices": "Devices — your LED controllers discovered on the network.", - "tour.tgt.css": "Color Strips — define how screen regions map to LED segments.", - "tour.tgt.targets": "LED Targets — combine a device, color strip, and capture source for streaming.", - "tour.tgt.kc_tab": "Key Colors — alternative target type using color-matching instead of pixel mapping.", - "tour.src.raw": "Raw — live screen capture sources from your displays.", - "tour.src.templates": "Capture Templates — reusable capture configurations (resolution, FPS, crop).", - "tour.src.static": "Static Image — test your setup with image files instead of live capture.", - "tour.src.processed": "Processed — apply post-processing effects like blur, brightness, or color correction.", - "tour.src.color_strip": "Color Strips — define how screen regions map to LED segments.", - "tour.src.audio": "Audio — analyze microphone or system audio for reactive LED effects.", - "tour.src.value": "Value — numeric data sources used as conditions in automations.", - "tour.src.sync": "Sync Clocks — shared timers that synchronize animations across multiple sources.", - "tour.auto.list": "Automations — automate scene activation based on time, audio, or value conditions.", - "tour.auto.add": "Click + to create a new automation with conditions and a scene to activate.", - "tour.auto.card": "Each card shows automation status, conditions, and quick controls to edit or toggle.", - "tour.auto.scenes_list": "Scenes — saved system states that automations can activate or you can apply manually.", - "tour.auto.scenes_add": "Click + to capture the current system state as a new scene preset.", - "tour.auto.scenes_card": "Each scene card shows target/device counts. Click to edit, recapture, or activate.", - "calibration.tutorial.start": "Start tutorial", - "calibration.overlay_toggle": "Overlay", - "calibration.start_position": "Starting Position:", - "calibration.position.bottom_left": "Bottom Left", - "calibration.position.bottom_right": "Bottom Right", - "calibration.position.top_left": "Top Left", - "calibration.position.top_right": "Top Right", - "calibration.direction": "Direction:", - "calibration.direction.clockwise": "Clockwise", - "calibration.direction.counterclockwise": "Counterclockwise", - "calibration.leds.top": "Top LEDs:", - "calibration.leds.right": "Right LEDs:", - "calibration.leds.bottom": "Bottom LEDs:", - "calibration.leds.left": "Left LEDs:", - "calibration.offset": "LED Offset:", - "calibration.offset.hint": "Distance from physical LED 0 to the start corner (along strip direction)", - "calibration.skip_start": "Skip LEDs (Start):", - "calibration.skip_start.hint": "Number of LEDs to turn off at the beginning of the strip (0 = none)", - "calibration.skip_end": "Skip LEDs (End):", - "calibration.skip_end.hint": "Number of LEDs to turn off at the end of the strip (0 = none)", - "calibration.border_width": "Border (px):", - "calibration.border_width.hint": "How many pixels from the screen edge to sample for LED colors (1-100)", - "calibration.button.cancel": "Cancel", - "calibration.button.save": "Save", - "calibration.saved": "Calibration saved", - "calibration.failed": "Failed to save calibration", - "server.healthy": "Server online", - "server.offline": "Server offline", - "error.unauthorized": "Unauthorized - please login", - "error.network": "Network error", - "error.unknown": "An error occurred", - "modal.discard_changes": "You have unsaved changes. Discard them?", - "confirm.title": "Confirm Action", - "confirm.yes": "Yes", - "confirm.no": "No", - "confirm.stop_all": "Stop all running targets?", - "confirm.turn_off_device": "Turn off this device?", - "common.loading": "Loading...", - "common.delete": "Delete", - "common.remove": "Remove", - "common.edit": "Edit", - "common.clone": "Clone", - "common.none": "None", - "common.none_no_cspt": "None (no processing template)", - "common.none_no_input": "None (no input source)", - "common.none_own_speed": "None (no sync)", - "common.undo": "Undo", - "validation.required": "This field is required", - "bulk.processing": "Processing…", - "api.error.timeout": "Request timed out — please try again", - "api.error.network": "Network error — check your connection", - "palette.search": "Search…", - "section.filter.placeholder": "Filter...", - "section.filter.reset": "Clear filter", - "tags.label": "Tags", - "tags.hint": "Assign tags for grouping and filtering cards", - "tags.placeholder": "Add tag...", - "section.expand_all": "Expand all sections", - "section.collapse_all": "Collapse all sections", - "streams.title": "Sources", - "streams.description": "Sources define the capture pipeline. A raw source captures from a display using a capture template. A processed source applies postprocessing to another source. Assign sources to devices.", - "streams.group.raw": "Sources", - "streams.group.raw_templates": "Engine Templates", - "streams.group.processed": "Sources", - "streams.group.proc_templates": "Filter Templates", - "streams.group.css_processing": "Processing Templates", - "streams.group.color_strip": "Color Strips", - "streams.group.audio": "Audio", - "streams.group.audio_templates": "Audio Templates", - "streams.section.streams": "Sources", - "streams.add": "Add Source", - "streams.add.raw": "Add Screen Capture", - "streams.add.processed": "Add Processed Source", - "streams.edit": "Edit Source", - "streams.edit.raw": "Edit Screen Capture", - "streams.edit.processed": "Edit Processed Source", - "streams.name": "Source Name:", - "streams.name.placeholder": "My Source", - "streams.type": "Type:", - "streams.type.raw": "Screen Capture", - "streams.type.processed": "Processed", - "streams.display": "Display:", - "streams.display.hint": "Which screen to capture", - "streams.capture_template": "Engine Template:", - "streams.capture_template.hint": "Engine template defining how the screen is captured", - "streams.target_fps": "Target FPS:", - "streams.target_fps.hint": "Target frames per second for capture (1-90)", - "streams.source": "Source:", - "streams.source.hint": "The source to apply processing filters to", - "streams.pp_template": "Filter Template:", - "streams.pp_template.hint": "Filter template to apply to the source", - "streams.description_label": "Description (optional):", - "streams.description_placeholder": "Describe this source...", - "streams.created": "Source created successfully", - "streams.updated": "Source updated successfully", - "streams.deleted": "Source deleted successfully", - "streams.delete.confirm": "Are you sure you want to delete this source?", - "streams.modal.loading": "Loading...", - "streams.error.load": "Failed to load sources", - "streams.error.required": "Please fill in all required fields", - "streams.error.delete": "Failed to delete source", - "streams.test.title": "Test Source", - "streams.test.run": "Run", - "streams.test.running": "Testing source...", - "streams.test.duration": "Capture Duration (s):", - "streams.test.error.failed": "Source test failed", - "postprocessing.title": "Filter Templates", - "postprocessing.description": "Processing templates define image filters and color correction. Assign them to processed picture sources for consistent postprocessing across devices.", - "postprocessing.add": "Add Filter Template", - "postprocessing.edit": "Edit Filter Template", - "postprocessing.name": "Template Name:", - "postprocessing.name.placeholder": "My Filter Template", - "filters.select_type": "Select filter type...", - "filters.add": "Add Filter", - "filters.remove": "Remove", - "filters.drag_to_reorder": "Drag to reorder", - "filters.empty": "No filters added. Use the selector below to add filters.", - "filters.brightness": "Brightness", - "filters.brightness.desc": "Adjust overall image brightness", - "filters.saturation": "Saturation", - "filters.saturation.desc": "Boost or reduce color intensity", - "filters.gamma": "Gamma", - "filters.gamma.desc": "Non-linear brightness curve correction", - "filters.downscaler": "Downscaler", - "filters.downscaler.desc": "Reduce resolution for faster processing", - "filters.pixelate": "Pixelate", - "filters.pixelate.desc": "Mosaic-style block averaging", - "filters.auto_crop": "Auto Crop", - "filters.auto_crop.desc": "Remove black bars from letterboxed content", - "filters.flip": "Flip", - "filters.flip.desc": "Mirror image horizontally or vertically", - "filters.color_correction": "Color Correction", - "filters.color_correction.desc": "White balance and color temperature", - "filters.filter_template": "Filter Template", - "filters.filter_template.desc": "Embed another processing template", - "filters.css_filter_template": "Strip Filter Template", - "filters.css_filter_template.desc": "Embed another strip processing template", - "filters.frame_interpolation": "Frame Interpolation", - "filters.frame_interpolation.desc": "Blend between frames for smoother output", - "filters.noise_gate": "Noise Gate", - "filters.noise_gate.desc": "Suppress small color changes below threshold", - "filters.palette_quantization": "Palette Quantization", - "filters.palette_quantization.desc": "Reduce colors to a limited palette", - "filters.reverse": "Reverse", - "filters.reverse.desc": "Reverse the LED order in the strip", - "postprocessing.description_label": "Description (optional):", - "postprocessing.description_placeholder": "Describe this template...", - "postprocessing.created": "Template created successfully", - "postprocessing.updated": "Template updated successfully", - "postprocessing.deleted": "Template deleted successfully", - "postprocessing.delete.confirm": "Are you sure you want to delete this filter template?", - "postprocessing.error.load": "Failed to load processing templates", - "postprocessing.error.required": "Please fill in all required fields", - "postprocessing.error.delete": "Failed to delete processing template", - "postprocessing.config.show": "Show settings", - "postprocessing.test.title": "Test Filter Template", - "postprocessing.test.source_stream": "Source:", - "postprocessing.test.running": "Testing processing template...", - "postprocessing.test.error.no_stream": "Please select a source", - "postprocessing.test.error.failed": "Processing template test failed", - "css_processing.title": "Strip Processing Templates", - "css_processing.add": "Add Strip Processing Template", - "css_processing.edit": "Edit Strip Processing Template", - "css_processing.name": "Template Name:", - "css_processing.name_placeholder": "My Strip Processing Template", - "css_processing.description_label": "Description (optional):", - "css_processing.description_placeholder": "Describe this template...", - "css_processing.created": "Strip processing template created", - "css_processing.updated": "Strip processing template updated", - "css_processing.deleted": "Strip processing template deleted", - "css_processing.delete.confirm": "Are you sure you want to delete this strip processing template?", - "css_processing.error.required": "Please fill in all required fields", - "css_processing.error.load": "Error loading strip processing template", - "css_processing.error.delete": "Error deleting strip processing template", - "css_processing.error.clone_failed": "Failed to clone strip processing template", - "device.button.stream_selector": "Source Settings", - "device.stream_settings.title": "Source Settings", - "device.stream_selector.label": "Source:", - "device.stream_selector.hint": "Select a source that defines what this device captures and processes", - "device.stream_selector.none": "-- No source assigned --", - "device.stream_selector.saved": "Source settings updated", - "device.stream_settings.border_width": "Border Width (px):", - "device.stream_settings.border_width_hint": "How many pixels from the screen edge to sample for LED colors (1-100)", - "device.stream_settings.interpolation": "Interpolation Mode:", - "device.stream_settings.interpolation.average": "Average", - "device.stream_settings.interpolation.median": "Median", - "device.stream_settings.interpolation.dominant": "Dominant", - "device.stream_settings.interpolation_hint": "How to calculate LED color from sampled pixels", - "device.stream_settings.smoothing": "Smoothing:", - "device.stream_settings.smoothing_hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.", - "device.tip.stream_selector": "Configure picture source and LED projection settings for this device", - "streams.group.static_image": "Static Image", - "streams.add.static_image": "Add Static Image Source", - "streams.edit.static_image": "Edit Static Image Source", - "streams.type.static_image": "Static Image", - "streams.group.video": "Video", - "streams.add.video": "Add Video Source", - "streams.edit.video": "Edit Video Source", - "picture_source.type.video": "Video", - "picture_source.type.video.desc": "Stream frames from an uploaded video asset", - "picture_source.video.loop": "Loop:", - "picture_source.video.speed": "Playback Speed:", - "picture_source.video.start_time": "Start Time (s):", - "picture_source.video.end_time": "End Time (s):", - "picture_source.video.resolution_limit": "Max Width (px):", - "picture_source.video.resolution_limit.hint": "Downscale video at decode time for performance", - "streams.image_asset": "Image Asset:", - "streams.image_asset.select": "Select image asset…", - "streams.image_asset.search": "Search image assets…", - "streams.video_asset": "Video Asset:", - "streams.video_asset.select": "Select video asset…", - "streams.video_asset.search": "Search video assets…", - "targets.title": "Targets", - "targets.description": "Targets bridge color strip sources to output devices. Each target references a device and a color strip source.", - "targets.subtab.wled": "LED", - "targets.subtab.led": "LED", - "targets.section.devices": "Devices", - "targets.section.color_strips": "Color Strip Sources", - "targets.section.targets": "Targets", - "targets.section.specific_settings": "Specific Settings", - "targets.section.advanced": "Advanced", - "targets.add": "Add Target", - "targets.edit": "Edit Target", - "targets.loading": "Loading targets...", - "targets.none": "No targets configured", - "targets.failed": "Failed to load targets", - "targets.name": "Target Name:", - "targets.name.placeholder": "My Target", - "targets.device": "Device:", - "targets.device.hint": "Select the LED device to send data to", - "targets.device.none": "-- Select a device --", - "targets.color_strip_source": "Color Strip Source:", - "targets.color_strip_source.hint": "Select the color strip source that provides LED colors for this target", - "targets.no_css": "No source", - "targets.source": "Source:", - "targets.source.hint": "Which picture source to capture and process", - "targets.source.none": "-- No source assigned --", - "targets.metrics.pipeline": "Pipeline details", - "targets.fps": "Target FPS:", - "targets.fps.hint": "Target frames per second for capture and LED updates (1-90)", - "targets.fps.rec": "Hardware max ≈ {fps} fps ({leds} LEDs)", - "targets.border_width": "Border Width (px):", - "targets.border_width.hint": "How many pixels from the screen edge to sample for LED colors (1-100)", - "targets.interpolation": "Interpolation Mode:", - "targets.interpolation.hint": "How to calculate LED color from sampled pixels", - "targets.interpolation.average": "Average", - "targets.interpolation.median": "Median", - "targets.interpolation.dominant": "Dominant", - "targets.smoothing": "Smoothing:", - "targets.smoothing.hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.", - "targets.keepalive_interval": "Keep Alive Interval:", - "targets.keepalive_interval.hint": "How often to resend the last frame when the source is static, keeping the device in live mode (0.5-5.0s)", - "targets.created": "Target created successfully", - "targets.updated": "Target updated successfully", - "targets.deleted": "Target deleted successfully", - "targets.delete.confirm": "Are you sure you want to delete this target?", - "targets.error.load": "Failed to load targets", - "targets.error.required": "Please fill in all required fields", - "targets.error.name_required": "Please enter a target name", - "targets.error.delete": "Failed to delete target", - "targets.button.start": "Start", - "targets.button.stop": "Stop", - "targets.status.processing": "Processing", - "targets.status.idle": "Idle", - "targets.status.error": "Error", - "targets.metrics.actual_fps": "Actual FPS", - "targets.metrics.target_fps": "Target FPS", - "targets.metrics.frames": "Frames", - "targets.metrics.errors": "Errors", - "targets.subtab.key_colors": "Key Colors", - "targets.section.key_colors": "Key Colors Targets", - "kc.add": "Add Key Colors Target", - "kc.edit": "Edit Key Colors Target", - "kc.name": "Target Name:", - "kc.name.placeholder": "My Key Colors Target", - "kc.source": "Picture Source:", - "kc.source.hint": "Which picture source to extract colors from", - "kc.source.none": "-- No source assigned --", - "kc.fps": "Extraction FPS:", - "kc.fps.hint": "How many times per second to extract colors (1-60)", - "kc.interpolation": "Color Mode:", - "kc.interpolation.hint": "How to compute the key color from pixels in each rectangle", - "kc.interpolation.average": "Average", - "kc.interpolation.median": "Median", - "kc.interpolation.dominant": "Dominant", - "kc.interpolation.average.desc": "Mean of all pixel colors", - "kc.interpolation.median.desc": "Middle color value per channel", - "kc.interpolation.dominant.desc": "Most frequent color", - "kc.smoothing": "Smoothing:", - "kc.smoothing.hint": "Temporal blending between extractions (0=none, 1=full)", - "kc.pattern_template": "Pattern Template:", - "kc.pattern_template.hint": "Select the rectangle pattern to use for color extraction", - "kc.pattern_template.none": "-- Select a pattern template --", - "kc.brightness_vs": "Brightness Source:", - "kc.brightness_vs.hint": "Optional value source that dynamically controls brightness each frame (multiplied with the manual brightness slider)", - "kc.brightness_vs.none": "None (manual brightness only)", - "kc.created": "Key colors target created successfully", - "kc.updated": "Key colors target updated successfully", - "kc.deleted": "Key colors target deleted successfully", - "kc.delete.confirm": "Are you sure you want to delete this key colors target?", - "kc.error.no_pattern": "Please select a pattern template", - "kc.error.required": "Please fill in all required fields", - "kc.colors.none": "No colors extracted yet", - "kc.test": "Test", - "kc.test.error": "Test failed", - "targets.section.pattern_templates": "Pattern Templates", - "pattern.add": "Add Pattern Template", - "pattern.edit": "Edit Pattern Template", - "pattern.name": "Template Name:", - "pattern.name.placeholder": "My Pattern Template", - "pattern.description_label": "Description (optional):", - "pattern.description_placeholder": "Describe this pattern...", - "pattern.rectangles": "Rectangles", - "pattern.rect.name": "Name", - "pattern.rect.x": "X", - "pattern.rect.y": "Y", - "pattern.rect.width": "W", - "pattern.rect.height": "H", - "pattern.rect.add": "Add Rectangle", - "pattern.rect.remove": "Remove", - "pattern.rect.empty": "No rectangles defined. Add at least one rectangle.", - "pattern.created": "Pattern template created successfully", - "pattern.updated": "Pattern template updated successfully", - "pattern.deleted": "Pattern template deleted successfully", - "pattern.delete.confirm": "Are you sure you want to delete this pattern template?", - "pattern.delete.referenced": "Cannot delete: this template is referenced by a target", - "pattern.error.required": "Please fill in all required fields", - "pattern.visual_editor": "Visual Editor", - "pattern.capture_bg": "Capture Background", - "pattern.source_for_bg": "Source for Background:", - "pattern.source_for_bg.none": "-- Select source --", - "pattern.delete_selected": "Delete Selected", - "pattern.name.hint": "A descriptive name for this rectangle layout", - "pattern.description.hint": "Optional notes about where or how this pattern is used", - "pattern.visual_editor.hint": "Click + buttons to add rectangles. Drag edges to resize, drag inside to move.", - "pattern.rectangles.hint": "Fine-tune rectangle positions and sizes with exact coordinates (0.0 to 1.0)", - "overlay.toggle": "Toggle screen overlay", - "overlay.button.show": "Show overlay visualization", - "overlay.button.hide": "Hide overlay visualization", - "overlay.started": "Overlay visualization started", - "overlay.stopped": "Overlay visualization stopped", - "overlay.error.start": "Failed to start overlay", - "overlay.error.stop": "Failed to stop overlay", - "dashboard.title": "Dashboard", - "dashboard.section.targets": "Targets", - "dashboard.section.running": "Running", - "dashboard.section.stopped": "Stopped", - "dashboard.no_targets": "No targets configured", - "dashboard.uptime": "Uptime", - "dashboard.fps": "FPS", - "dashboard.errors": "Errors", - "dashboard.device": "Device", - "dashboard.stop_all": "Stop All", - "dashboard.failed": "Failed to load dashboard", - "dashboard.section.automations": "Automations", - "dashboard.section.scenes": "Scene Presets", - "dashboard.section.sync_clocks": "Sync Clocks", - "dashboard.targets": "Targets", - "dashboard.section.performance": "System Performance", - "dashboard.perf.cpu": "CPU", - "dashboard.perf.ram": "RAM", - "dashboard.perf.gpu": "GPU", - "dashboard.perf.unavailable": "unavailable", - "dashboard.perf.color": "Chart color", - "dashboard.perf.mode.system": "System", - "dashboard.perf.mode.app": "App", - "dashboard.perf.mode.both": "Both", - "dashboard.poll_interval": "Refresh interval", - "automations.title": "Automations", - "automations.empty": "No automations configured. Create one to automate scene activation.", - "automations.add": "Add Automation", - "automations.edit": "Edit Automation", - "automations.delete.confirm": "Delete automation \"{name}\"?", - "automations.name": "Name:", - "automations.name.hint": "A descriptive name for this automation", - "automations.name.placeholder": "My Automation", - "automations.enabled": "Enabled:", - "automations.enabled.hint": "Disabled automations won't activate even when conditions are met", - "automations.condition_logic": "Condition Logic:", - "automations.condition_logic.hint": "How multiple conditions are combined: ANY (OR) or ALL (AND)", - "automations.condition_logic.or": "Any condition (OR)", - "automations.condition_logic.and": "All conditions (AND)", - "automations.condition_logic.or.desc": "Triggers when any condition matches", - "automations.condition_logic.and.desc": "Triggers only when all match", - "automations.conditions": "Conditions:", - "automations.conditions.hint": "Rules that determine when this automation activates", - "automations.conditions.add": "Add Condition", - "automations.conditions.empty": "No conditions — automation is always active when enabled", - "automations.condition.always": "Always", - "automations.condition.always.desc": "Always active", - "automations.condition.always.hint": "Automation activates immediately when enabled and stays active.", - "automations.condition.startup": "Startup", - "automations.condition.startup.desc": "On server start", - "automations.condition.startup.hint": "Activates when the server starts and stays active while enabled.", - "automations.condition.application": "Application", - "automations.condition.application.desc": "App running/focused", - "automations.condition.application.apps": "Applications:", - "automations.condition.application.apps.hint": "Process names, one per line (e.g. firefox.exe)", - "automations.condition.application.browse": "Browse", - "automations.condition.application.search": "Filter processes...", - "automations.condition.application.no_processes": "No processes found", - "automations.condition.application.match_type": "Match Type:", - "automations.condition.application.match_type.hint": "How to detect the application", - "automations.condition.application.match_type.running": "Running", - "automations.condition.application.match_type.running.desc": "Process is active", - "automations.condition.application.match_type.topmost": "Topmost", - "automations.condition.application.match_type.topmost.desc": "Foreground window", - "automations.condition.application.match_type.topmost_fullscreen": "Topmost + FS", - "automations.condition.application.match_type.topmost_fullscreen.desc": "Foreground + fullscreen", - "automations.condition.application.match_type.fullscreen": "Fullscreen", - "automations.condition.application.match_type.fullscreen.desc": "Any fullscreen app", - "automations.condition.time_of_day": "Time of Day", - "automations.condition.time_of_day.desc": "Time range", - "automations.condition.time_of_day.start_time": "Start Time:", - "automations.condition.time_of_day.end_time": "End Time:", - "automations.condition.time_of_day.overnight_hint": "For overnight ranges (e.g. 22:00–06:00), set start time after end time.", - "automations.condition.system_idle": "System Idle", - "automations.condition.system_idle.desc": "User idle/active", - "automations.condition.system_idle.idle_minutes": "Idle Timeout (minutes):", - "automations.condition.system_idle.mode": "Trigger Mode:", - "automations.condition.system_idle.when_idle": "When idle", - "automations.condition.system_idle.when_active": "When active", - "automations.condition.display_state": "Display State", - "automations.condition.display_state.desc": "Monitor on/off", - "automations.condition.display_state.state": "Monitor State:", - "automations.condition.display_state.on": "On", - "automations.condition.display_state.off": "Off (sleeping)", - "automations.condition.mqtt": "MQTT", - "automations.condition.mqtt.desc": "MQTT message", - "automations.condition.mqtt.topic": "Topic:", - "automations.condition.mqtt.payload": "Payload:", - "automations.condition.mqtt.match_mode": "Match Mode:", - "automations.condition.mqtt.match_mode.exact": "Exact", - "automations.condition.mqtt.match_mode.contains": "Contains", - "automations.condition.mqtt.match_mode.regex": "Regex", - "automations.condition.mqtt.hint": "Activate when an MQTT topic receives a matching payload", - "automations.condition.webhook": "Webhook", - "automations.condition.webhook.desc": "HTTP callback", - "automations.condition.webhook.hint": "Activate via an HTTP call from external services (Home Assistant, IFTTT, curl, etc.)", - "automations.condition.webhook.url": "Webhook URL:", - "automations.condition.webhook.copy": "Copy", - "automations.condition.webhook.copied": "Copied!", - "automations.condition.webhook.save_first": "Save the automation first to generate a webhook URL", - "automations.scene": "Scene:", - "automations.scene.hint": "Scene preset to activate when conditions are met", - "automations.scene.search_placeholder": "Search scenes...", - "automations.scene.none_selected": "None (no scene)", - "automations.scene.none_available": "No scenes available", - "automations.deactivation_mode": "Deactivation:", - "automations.deactivation_mode.hint": "What happens when conditions stop matching", - "automations.deactivation_mode.none": "None", - "automations.deactivation_mode.none.desc": "Keep current state", - "automations.deactivation_mode.revert": "Revert", - "automations.deactivation_mode.revert.desc": "Restore previous state", - "automations.deactivation_mode.fallback_scene": "Fallback", - "automations.deactivation_mode.fallback_scene.desc": "Activate a fallback scene", - "automations.deactivation_scene": "Fallback Scene:", - "automations.deactivation_scene.hint": "Scene to activate when this automation deactivates", - "automations.status.active": "Active", - "automations.status.inactive": "Inactive", - "automations.status.disabled": "Disabled", - "automations.action.disable": "Disable", - "automations.last_activated": "Last activated", - "automations.logic.and": " AND ", - "automations.logic.or": " OR ", - "automations.logic.all": "ALL", - "automations.logic.any": "ANY", - "automations.updated": "Automation updated", - "automations.created": "Automation created", - "automations.deleted": "Automation deleted", - "automations.error.name_required": "Name is required", - "automations.error.clone_failed": "Failed to clone automation", - "scenes.title": "Scenes", - "scenes.add": "Capture Scene", - "scenes.edit": "Edit Scene", - "scenes.name": "Name:", - "scenes.name.hint": "A descriptive name for this scene preset", - "scenes.name.placeholder": "My Scene", - "scenes.description": "Description:", - "scenes.description.hint": "Optional description of what this scene does", - "scenes.targets": "Targets:", - "scenes.targets.hint": "Select which targets to include in this scene snapshot", - "scenes.targets.add": "Add Target", - "scenes.targets.search_placeholder": "Search targets...", - "scenes.capture": "Capture", - "scenes.activate": "Activate scene", - "scenes.recapture": "Recapture current state", - "scenes.delete": "Delete scene", - "scenes.targets_count": "targets", - "scenes.captured": "Scene captured", - "scenes.updated": "Scene updated", - "scenes.activated": "Scene activated", - "scenes.activated_partial": "Scene partially activated", - "scenes.errors": "errors", - "scenes.recaptured": "Scene recaptured", - "scenes.deleted": "Scene deleted", - "scenes.recapture_confirm": "Recapture current state into \"{name}\"?", - "scenes.delete_confirm": "Delete scene \"{name}\"?", - "scenes.error.name_required": "Name is required", - "scenes.error.save_failed": "Failed to save scene", - "scenes.error.activate_failed": "Failed to activate scene", - "scenes.error.recapture_failed": "Failed to recapture scene", - "scenes.error.delete_failed": "Failed to delete scene", - "scenes.cloned": "Scene cloned", - "scenes.error.clone_failed": "Failed to clone scene", - "time.hours_minutes": "{h}h {m}m", - "time.minutes_seconds": "{m}m {s}s", - "time.seconds": "{s}s", - "dashboard.type.led": "LED", - "dashboard.type.kc": "Key Colors", - "aria.close": "Close", - "aria.save": "Save", - "aria.cancel": "Cancel", - "aria.previous": "Previous", - "aria.next": "Next", - "aria.hint": "Show hint", - "color_strip.select_type": "Select Color Strip Type", - "color_strip.add": "Add", - "color_strip.edit": "Edit", - "color_strip.name": "Name:", - "color_strip.name.placeholder": "Wall Strip", - "color_strip.picture_source": "Picture Source:", - "color_strip.picture_source.hint": "Which screen capture source to use as input for LED color calculation", - "color_strip.fps": "Target FPS:", - "color_strip.fps.hint": "Target frames per second for LED color updates (10-90)", - "color_strip.interpolation": "Color Mode:", - "color_strip.interpolation.hint": "How to calculate LED color from sampled border pixels", - "color_strip.interpolation.average": "Average", - "color_strip.interpolation.median": "Median", - "color_strip.interpolation.dominant": "Dominant", - "color_strip.interpolation.average.desc": "Blend all sampled pixels into a smooth color", - "color_strip.interpolation.median.desc": "Pick the middle color, reducing outliers", - "color_strip.interpolation.dominant.desc": "Use the most frequent color in the sample", - "color_strip.smoothing": "Smoothing:", - "color_strip.smoothing.hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.", - "color_strip.frame_interpolation": "Frame Interpolation:", - "color_strip.frame_interpolation.hint": "Blends between consecutive captured frames to produce output at the full target FPS even when capture rate is lower. Reduces visible stepping on slow ambient transitions.", - "color_strip.color_corrections": "Color Corrections", - "color_strip.brightness": "Brightness:", - "color_strip.brightness.hint": "Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.", - "color_strip.saturation": "Saturation:", - "color_strip.saturation.hint": "Color saturation (0=grayscale, 1=unchanged, 2=double saturation)", - "color_strip.gamma": "Gamma:", - "color_strip.gamma.hint": "Gamma correction (1=none, <1=brighter midtones, >1=darker midtones)", - "color_strip.test_device": "Test on Device:", - "color_strip.test_device.hint": "Select a device to send test pixels to when clicking edge toggles", - "color_strip.leds": "LED count", - "color_strip.led_count": "LED Count:", - "color_strip.led_count.hint": "Total number of LEDs on the physical strip. For screen sources: 0 = auto from calibration (extra LEDs not mapped to edges will be black). For static color: set to match your device LED count.", - "color_strip.created": "Color strip source created", - "color_strip.updated": "Color strip source updated", - "color_strip.deleted": "Color strip source deleted", - "color_strip.delete.confirm": "Are you sure you want to delete this color strip source?", - "color_strip.delete.referenced": "Cannot delete: this source is in use by a target", - "color_strip.error.name_required": "Please enter a name", - "color_strip.type": "Type:", - "color_strip.type.hint": "Picture Source derives LED colors from a screen capture. Static Color fills all LEDs with a single constant color. Gradient distributes a color gradient across all LEDs. Color Cycle smoothly cycles through a user-defined list of colors. Composite stacks multiple sources as blended layers. Audio Reactive drives LEDs from real-time audio input. API Input receives raw LED colors from external clients via REST or WebSocket.", - "color_strip.type.picture": "Picture Source", - "color_strip.type.picture.desc": "Colors from screen capture", - "color_strip.type.picture_advanced": "Multi-Monitor", - "color_strip.type.picture_advanced.desc": "Line-based calibration across monitors", - "color_strip.type.static": "Static Color", - "color_strip.type.static.desc": "Single solid color fill", - "color_strip.type.gradient": "Gradient", - "color_strip.type.gradient.desc": "Smooth color transition across LEDs", - "color_strip.type.color_cycle": "Color Cycle", - "color_strip.type.color_cycle.desc": "Cycle through a list of colors", - "color_strip.static_color": "Color:", - "color_strip.static_color.hint": "The solid color that will be sent to all LEDs on the strip.", - "color_strip.gradient.preview": "Gradient:", - "color_strip.gradient.preview.hint": "Visual preview. Click the marker track below to add a stop. Drag markers to reposition.", - "color_strip.gradient.easing": "Easing:", - "color_strip.gradient.easing.hint": "Controls how colors blend between gradient stops.", - "color_strip.gradient.easing.linear": "Linear", - "color_strip.gradient.easing.linear.desc": "Constant-rate blending between stops", - "color_strip.gradient.easing.ease_in_out": "Smooth", - "color_strip.gradient.easing.ease_in_out.desc": "S-curve: slow start and end, fast middle", - "color_strip.gradient.easing.step": "Step", - "color_strip.gradient.easing.step.desc": "Hard jumps between colors with no blending", - "color_strip.gradient.easing.cubic": "Cubic", - "color_strip.gradient.easing.cubic.desc": "Cubic ease — accelerating blend curve", - "color_strip.gradient.stops": "Color Stops:", - "color_strip.gradient.stops.hint": "Each stop defines a color at a relative position (0.0 = start, 1.0 = end). The ↔ button adds a right-side color to create a hard edge at that stop.", - "color_strip.gradient.stops_count": "stops", - "color_strip.gradient.add_stop": "+ Add Stop", - "color_strip.gradient.position": "Position (0.0–1.0)", - "color_strip.gradient.bidir.hint": "Add a second color on the right side of this stop to create a hard edge in the gradient.", - "color_strip.gradient.min_stops": "Gradient must have at least 2 stops", - "color_strip.gradient.select": "Gradient:", - "color_strip.gradient.select.hint": "Select a gradient from the library. Create and edit gradients in the Gradients tab.", - "color_strip.gradient.error.no_gradient": "Please select a gradient", - "color_strip.gradient.preset": "Preset:", - "color_strip.gradient.preset.hint": "Load a predefined gradient palette. Selecting a preset replaces the current stops.", - "color_strip.gradient.preset.custom": "— Custom —", - "color_strip.gradient.preset.rainbow": "Rainbow", - "color_strip.gradient.preset.sunset": "Sunset", - "color_strip.gradient.preset.ocean": "Ocean", - "color_strip.gradient.preset.forest": "Forest", - "color_strip.gradient.preset.fire": "Fire", - "color_strip.gradient.preset.lava": "Lava", - "color_strip.gradient.preset.aurora": "Aurora", - "color_strip.gradient.preset.ice": "Ice", - "color_strip.gradient.preset.warm": "Warm", - "color_strip.gradient.preset.cool": "Cool", - "color_strip.gradient.preset.neon": "Neon", - "color_strip.gradient.preset.pastel": "Pastel", - "color_strip.gradient.preset.save_button": "Save as preset…", - "color_strip.gradient.preset.save_prompt": "Enter a name for this preset:", - "color_strip.gradient.preset.saved": "Preset saved", - "color_strip.gradient.preset.deleted": "Preset deleted", - "color_strip.gradient.preset.apply": "Apply", - "color_strip.animation": "Animation", - "color_strip.animation.type": "Effect:", - "color_strip.animation.type.hint": "Animation effect to apply.", - "color_strip.animation.type.none": "None (no animation effect)", - "color_strip.animation.type.none.desc": "Static colors with no animation", - "color_strip.animation.type.breathing": "Breathing", - "color_strip.animation.type.breathing.desc": "Smooth brightness fade in and out", - "color_strip.animation.type.color_cycle": "Color Cycle", - "color_strip.animation.type.gradient_shift": "Gradient Shift", - "color_strip.animation.type.gradient_shift.desc": "Slides the gradient along the strip", - "color_strip.animation.type.wave": "Wave", - "color_strip.animation.type.wave.desc": "Sinusoidal brightness wave moving along the strip", - "color_strip.animation.type.strobe": "Strobe", - "color_strip.animation.type.strobe.desc": "Rapid on/off flashing", - "color_strip.animation.type.sparkle": "Sparkle", - "color_strip.animation.type.sparkle.desc": "Random LEDs flash briefly", - "color_strip.animation.type.pulse": "Pulse", - "color_strip.animation.type.pulse.desc": "Sharp brightness pulse with quick fade", - "color_strip.animation.type.candle": "Candle", - "color_strip.animation.type.candle.desc": "Warm flickering candle-like glow", - "color_strip.animation.type.rainbow_fade": "Rainbow Fade", - "color_strip.animation.type.rainbow_fade.desc": "Cycles through the entire hue spectrum", - "color_strip.animation.type.noise_perturb": "Noise Perturb", - "color_strip.animation.type.noise_perturb.desc": "Perturbs gradient stop positions with organic noise each frame", - "color_strip.animation.type.hue_rotate": "Hue Rotate", - "color_strip.animation.type.hue_rotate.desc": "Smoothly rotates all pixel hues while preserving saturation and brightness", - "color_strip.animation.speed": "Speed:", - "color_strip.animation.speed.hint": "Animation speed multiplier. 1.0 ≈ one cycle per second for Breathing; higher values cycle faster.", - "color_strip.color_cycle.colors": "Colors:", - "color_strip.color_cycle.colors.hint": "List of colors to cycle through smoothly. At least 2 required. Default is a full rainbow spectrum.", - "color_strip.color_cycle.add_color": "+ Add Color", - "color_strip.color_cycle.speed": "Speed:", - "color_strip.color_cycle.speed.hint": "Cycle speed multiplier. 1.0 ≈ one full cycle every 20 seconds; higher values cycle faster.", - "color_strip.color_cycle.min_colors": "Color cycle must have at least 2 colors", - "color_strip.type.effect": "Effect", - "color_strip.type.effect.desc": "Procedural effects like fire, plasma, aurora", - "color_strip.type.effect.hint": "Procedural LED effects (fire, meteor, plasma, noise, aurora) generated in real time.", - "color_strip.type.composite": "Composite", - "color_strip.type.composite.desc": "Stack and blend multiple sources", - "color_strip.type.composite.hint": "Stack multiple color strip sources as layers with blend modes and opacity.", - "color_strip.type.mapped": "Mapped", - "color_strip.type.mapped.desc": "Assign sources to LED zones", - "color_strip.type.mapped.hint": "Assign different color strip sources to different LED ranges (zones). Unlike composite which blends layers, mapped places sources side-by-side.", - "color_strip.type.audio": "Audio Reactive", - "color_strip.type.audio.desc": "LEDs driven by audio input", - "color_strip.type.audio.hint": "LED colors driven by real-time audio input — system audio or microphone.", - "color_strip.type.api_input": "API Input", - "color_strip.type.api_input.desc": "Receive colors from external apps", - "color_strip.type.api_input.hint": "Receives raw LED color arrays from external clients via REST POST or WebSocket. Use this to integrate with custom software, home automation, or any system that can send HTTP requests.", - "color_strip.api_input.fallback_color": "Fallback Color:", - "color_strip.api_input.fallback_color.hint": "Color to display when no data has been received within the timeout period. LEDs will show this color on startup and after the connection is lost.", - "color_strip.api_input.timeout": "Timeout (seconds):", - "color_strip.api_input.timeout.hint": "How long to wait for new color data before reverting to the fallback color. Set to 0 to never time out.", - "color_strip.api_input.endpoints": "Push Endpoints:", - "color_strip.api_input.endpoints.hint": "Use these URLs to push LED color data from your external application. REST accepts JSON, WebSocket accepts both JSON and raw binary frames.", - "color_strip.api_input.save_first": "Save the source first to see the push endpoint URLs.", - "color_strip.api_input.interpolation": "LED Interpolation:", - "color_strip.api_input.interpolation.hint": "How to resize incoming LED data when its count differs from the device's LED count. Linear gives smooth blending, Nearest preserves sharp edges, None truncates or zero-pads.", - "color_strip.api_input.interpolation.linear": "Linear", - "color_strip.api_input.interpolation.linear.desc": "Smooth blending between LEDs", - "color_strip.api_input.interpolation.nearest": "Nearest", - "color_strip.api_input.interpolation.nearest.desc": "Sharp edges, no blending", - "color_strip.api_input.interpolation.none": "None", - "color_strip.api_input.interpolation.none.desc": "Truncate or zero-pad", - "color_strip.type.notification": "Notification", - "color_strip.type.notification.desc": "One-shot effect on webhook trigger", - "color_strip.type.notification.hint": "Fires a one-shot visual effect (flash, pulse, sweep) when triggered via a webhook. Designed for use as a composite layer over a persistent base source.", - "color_strip.notification.os_listener": "Listen to OS Notifications:", - "color_strip.notification.os_listener.hint": "When enabled, this source automatically fires when a desktop notification appears (Windows toast / Linux D-Bus). Requires the app to have notification access permission.", - "color_strip.notification.effect": "Effect:", - "color_strip.notification.effect.hint": "Visual effect when a notification fires. Flash fades linearly, Pulse uses a smooth bell curve, Sweep fills LEDs left-to-right then fades.", - "color_strip.notification.effect.flash": "Flash", - "color_strip.notification.effect.flash.desc": "Instant on, linear fade-out", - "color_strip.notification.effect.pulse": "Pulse", - "color_strip.notification.effect.pulse.desc": "Smooth bell-curve glow", - "color_strip.notification.effect.sweep": "Sweep", - "color_strip.notification.effect.sweep.desc": "Fills left-to-right then fades", - "color_strip.notification.effect.chase": "Chase", - "color_strip.notification.effect.chase.desc": "Light travels across strip and bounces back", - "color_strip.notification.effect.gradient_flash": "Gradient Flash", - "color_strip.notification.effect.gradient_flash.desc": "Bright center fades to edges, then all fades out", - "color_strip.notification.duration": "Duration (ms):", - "color_strip.notification.duration.hint": "How long the notification effect plays, in milliseconds.", - "color_strip.notification.default_color": "Default Color:", - "color_strip.notification.default_color.hint": "Color used when the notification has no app-specific color mapping.", - "color_strip.notification.filter_mode": "App Filter:", - "color_strip.notification.filter_mode.hint": "Filter notifications by app name. Off = accept all, Whitelist = only listed apps, Blacklist = all except listed apps.", - "color_strip.notification.filter_mode.off": "Off", - "color_strip.notification.filter_mode.whitelist": "Whitelist", - "color_strip.notification.filter_mode.blacklist": "Blacklist", - "color_strip.notification.filter_mode.off.desc": "Accept all notifications", - "color_strip.notification.filter_mode.whitelist.desc": "Only listed apps", - "color_strip.notification.filter_mode.blacklist.desc": "All except listed apps", - "color_strip.notification.filter_list": "App List:", - "color_strip.notification.filter_list.hint": "One app name per line. Use Browse to pick from running processes.", - "color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram", - "color_strip.notification.app_colors": "App Colors", - "color_strip.notification.app_colors.label": "Color Mappings:", - "color_strip.notification.app_colors.hint": "Per-app color overrides. Each row maps an app name to a specific notification color.", - "color_strip.notification.app_colors.add": "+ Add Mapping", - "color_strip.notification.app_overrides": "Per-App Overrides", - "color_strip.notification.app_overrides.label": "App Overrides:", - "color_strip.notification.app_overrides.hint": "Per-app overrides for color and sound. Each row can set a custom color, sound asset, and volume for a specific app.", - "color_strip.notification.app_overrides.add": "+ Add Override", - "color_strip.notification.app_overrides.app_placeholder": "App name", - "color_strip.notification.sound": "Sound", - "color_strip.notification.sound.asset": "Sound Asset:", - "color_strip.notification.sound.asset.hint": "Pick a sound asset to play when a notification fires. Leave empty for silent.", - "color_strip.notification.sound.none": "None (silent)", - "color_strip.notification.sound.search": "Search sounds…", - "color_strip.notification.sound.volume": "Volume:", - "color_strip.notification.sound.volume.hint": "Global volume for notification sounds (0–100%).", - "color_strip.notification.sound.app_sounds": "Per-App Sounds:", - "color_strip.notification.sound.app_sounds.hint": "Override sound and volume for specific apps. Empty sound = mute that app.", - "color_strip.notification.sound.app_sounds.add": "+ Add Override", - "color_strip.notification.sound.app_name_placeholder": "App name", - "color_strip.notification.endpoint": "Webhook Endpoint:", - "color_strip.notification.endpoint.hint": "Use this URL to trigger notifications from external systems. POST with optional JSON body: {\"app\": \"AppName\", \"color\": \"#FF0000\"}.", - "color_strip.notification.save_first": "Save the source first to see the webhook endpoint URL.", - "color_strip.notification.app_count": "apps", - "color_strip.notification.test": "Test notification", - "color_strip.notification.test.ok": "Notification sent", - "color_strip.notification.test.no_streams": "No running streams for this source", - "color_strip.notification.test.error": "Failed to send notification", - "color_strip.notification.history.title": "Notification History", - "color_strip.notification.history.hint": "Recent OS notifications captured by the listener (newest first). Up to 50 entries.", - "color_strip.notification.history.empty": "No notifications captured yet", - "color_strip.notification.history.unavailable": "OS notification listener is not available on this platform", - "color_strip.notification.history.error": "Failed to load notification history", - "color_strip.notification.history.refresh": "Refresh", - "color_strip.notification.history.unknown_app": "Unknown app", - "color_strip.notification.history.fired": "Streams triggered", - "color_strip.notification.history.filtered": "Streams filtered", - "color_strip.test.title": "Test Preview", - "color_strip.test.connecting": "Connecting...", - "color_strip.test.error": "Failed to connect to preview stream", - "color_strip.test.led_count": "LEDs:", - "color_strip.test.fps": "FPS:", - "color_strip.test.receive_fps": "Receive FPS", - "color_strip.test.apply": "Apply", - "color_strip.test.composite": "Composite", - "color_strip.preview.title": "Live Preview", - "color_strip.preview.not_connected": "Not connected", - "color_strip.preview.connecting": "Connecting...", - "color_strip.preview.connected": "Connected", - "color_strip.preview.unsupported": "Preview not available for this source type", - "color_strip.type.daylight": "Daylight Cycle", - "color_strip.type.daylight.desc": "Simulates natural daylight over 24 hours", - "color_strip.type.daylight.hint": "Simulates the sun's color temperature throughout a 24-hour day/night cycle — from warm sunrise to cool daylight to warm sunset and dim night.", - "color_strip.daylight.speed": "Speed:", - "color_strip.daylight.speed.hint": "Cycle speed multiplier. 1.0 = full day/night cycle in ~4 minutes. Higher values cycle faster.", - "color_strip.daylight.use_real_time": "Use Real Time:", - "color_strip.daylight.use_real_time.hint": "When enabled, LED color matches the actual time of day on this computer. Speed setting is ignored.", - "color_strip.daylight.real_time": "Real Time", - "color_strip.daylight.latitude": "Latitude:", - "color_strip.daylight.latitude.hint": "Your geographic latitude (-90 to 90). Affects sunrise/sunset timing in real-time mode.", - "color_strip.daylight.longitude": "Longitude:", - "color_strip.daylight.longitude.hint": "Your geographic longitude (-180 to 180). Adjusts solar noon offset for accurate sunrise/sunset timing.", - "color_strip.type.candlelight": "Candlelight", - "color_strip.type.candlelight.desc": "Realistic flickering candle simulation", - "color_strip.type.candlelight.hint": "Simulates realistic candle flickering across all LEDs with warm tones and organic flicker patterns.", - "color_strip.type.key_colors": "Key Colors", - "color_strip.type.key_colors.desc": "Extract colors from screen regions", - "color_strip.key_colors.picture_source": "Picture Source:", - "color_strip.key_colors.interpolation": "Color Mode:", - "color_strip.key_colors.smoothing": "Smoothing:", - "color_strip.key_colors.brightness": "Brightness:", - "color_strip.key_colors.rectangles": "Screen Regions:", - "color_strip.key_colors.no_rects": "No regions defined. Click Configure Regions to add.", - "color_strip.key_colors.configure_regions": "Configure Regions", - "color_strip.key_colors.mode.average": "Average", - "color_strip.key_colors.mode.average.desc": "Mean color of all pixels in the region", - "color_strip.key_colors.mode.median": "Median", - "color_strip.key_colors.mode.median.desc": "Median color (less affected by outliers)", - "color_strip.key_colors.mode.dominant": "Dominant", - "color_strip.key_colors.mode.dominant.desc": "Most frequent color (K-means clustering)", - "color_strip.key_colors.error.no_source": "Picture source is required", - "color_strip.key_colors.error.no_rects": "At least one screen region is required", - "color_strip.type.weather": "Weather", - "color_strip.type.weather.desc": "Weather-reactive ambient colors", - "color_strip.type.weather.hint": "Maps real-time weather conditions to ambient LED colors. Requires a Weather Source entity.", - "color_strip.weather.source": "Weather Source:", - "color_strip.weather.source.hint": "The weather data source to use for ambient colors. Create one in the Weather tab first.", - "color_strip.weather.speed": "Animation Speed:", - "color_strip.weather.speed.hint": "Speed of the ambient color drift animation. Higher = faster movement.", - "color_strip.weather.temperature_influence": "Temperature Influence:", - "color_strip.weather.temperature_influence.hint": "How much the current temperature shifts the palette warm/cool. 0 = pure condition colors, 1 = strong shift.", - "color_strip.weather.error.no_source": "Please select a weather source", - "color_strip.candlelight.color": "Base Color:", - "color_strip.candlelight.color.hint": "The warm base color of the candle flame. Default is a natural warm amber.", - "color_strip.candlelight.intensity": "Flicker Intensity:", - "color_strip.candlelight.intensity.hint": "How much the candles flicker. Low values produce a gentle glow, high values simulate a windy candle.", - "color_strip.candlelight.num_candles_label": "Number of Candles:", - "color_strip.candlelight.num_candles": "candles", - "color_strip.candlelight.num_candles.hint": "How many independent candle sources along the strip. Each flickers with its own pattern.", - "color_strip.candlelight.speed": "Flicker Speed:", - "color_strip.candlelight.speed.hint": "Speed of the flicker animation. Higher values produce faster, more restless flames.", - "color_strip.candlelight.wind": "Wind:", - "color_strip.candlelight.wind.hint": "Wind simulation strength. Higher values create correlated gusts that make all candles flicker together.", - "color_strip.candlelight.type": "Candle Type:", - "color_strip.candlelight.type.hint": "Preset that adjusts flicker behavior without changing other settings.", - "color_strip.candlelight.type.default": "Default", - "color_strip.candlelight.type.default.desc": "Standard candle flicker", - "color_strip.candlelight.type.taper": "Taper", - "color_strip.candlelight.type.taper.desc": "Tall, steady candle with reduced flicker", - "color_strip.candlelight.type.votive": "Votive", - "color_strip.candlelight.type.votive.desc": "Small, flickery candle with narrow glow", - "color_strip.candlelight.type.bonfire": "Bonfire", - "color_strip.candlelight.type.bonfire.desc": "Large, chaotic fire with extra warmth", - "color_strip.type.processed": "Processed", - "color_strip.type.processed.desc": "Apply a processing template to another source", - "color_strip.type.processed.hint": "Wraps an existing color strip source and pipes its output through a filter chain.", - "color_strip.processed.input": "Source:", - "color_strip.processed.input.hint": "The color strip source whose output will be processed", - "color_strip.processed.template": "Processing Template:", - "color_strip.processed.template.hint": "Filter chain to apply to the input source output", - "color_strip.processed.error.no_input": "Please select an input source", - "color_strip.composite.layers": "Layers:", - "color_strip.composite.layers.hint": "Stack multiple color strip sources. First layer is the bottom, last is the top. Each layer can have its own blend mode and opacity.", - "color_strip.composite.add_layer": "+ Add Layer", - "color_strip.composite.source": "Source", - "color_strip.composite.blend_mode": "Blend", - "color_strip.composite.blend_mode.normal": "Normal", - "color_strip.composite.blend_mode.normal.desc": "Standard alpha blending", - "color_strip.composite.blend_mode.add": "Add", - "color_strip.composite.blend_mode.add.desc": "Brightens by adding colors", - "color_strip.composite.blend_mode.multiply": "Multiply", - "color_strip.composite.blend_mode.multiply.desc": "Darkens by multiplying colors", - "color_strip.composite.blend_mode.screen": "Screen", - "color_strip.composite.blend_mode.screen.desc": "Brightens, inverse of multiply", - "color_strip.composite.blend_mode.override": "Override", - "color_strip.composite.blend_mode.overlay": "Overlay", - "color_strip.composite.blend_mode.overlay.desc": "Multiply darks, screen lights", - "color_strip.composite.blend_mode.soft_light": "Soft Light", - "color_strip.composite.blend_mode.soft_light.desc": "Gentle contrast adjustment", - "color_strip.composite.blend_mode.hard_light": "Hard Light", - "color_strip.composite.blend_mode.hard_light.desc": "Strong contrast, vivid colors", - "color_strip.composite.blend_mode.difference": "Difference", - "color_strip.composite.blend_mode.difference.desc": "Absolute color difference", - "color_strip.composite.blend_mode.exclusion": "Exclusion", - "color_strip.composite.blend_mode.exclusion.desc": "Like difference, lower contrast", - "color_strip.composite.blend_mode.override.desc": "Black = transparent, bright = opaque", - "color_strip.composite.opacity": "Opacity", - "color_strip.composite.brightness": "Brightness", - "color_strip.composite.brightness.none": "None (full brightness)", - "color_strip.composite.processing": "Processing", - "color_strip.composite.enabled": "Enabled", - "color_strip.composite.error.min_layers": "At least 1 layer is required", - "color_strip.composite.error.no_source": "Each layer must have a source selected", - "color_strip.composite.layers_count": "layers", - "color_strip.composite.range": "LED Range", - "color_strip.composite.range_start": "Start", - "color_strip.composite.range_end": "End", - "color_strip.composite.reverse": "Reverse", - "color_strip.mapped.zones": "Zones:", - "color_strip.mapped.zones.hint": "Each zone maps a color strip source to a specific LED range. Zones are placed side-by-side — gaps between zones stay black.", - "color_strip.mapped.add_zone": "+ Add Zone", - "color_strip.mapped.zone_source": "Source", - "color_strip.mapped.zone_start": "Start LED", - "color_strip.mapped.zone_end": "End LED", - "color_strip.mapped.zone_reverse": "Reverse", - "color_strip.mapped.zones_count": "zones", - "color_strip.mapped.select_source": "Search sources...", - "color_strip.mapped.error.no_source": "Each zone must have a source selected", - "color_strip.audio.visualization": "Visualization:", - "color_strip.audio.visualization.hint": "How audio data is rendered to LEDs.", - "color_strip.audio.viz.spectrum": "Spectrum Analyzer", - "color_strip.audio.viz.spectrum.desc": "Frequency bars across the strip", - "color_strip.audio.viz.beat_pulse": "Beat Pulse", - "color_strip.audio.viz.beat_pulse.desc": "All LEDs pulse on the beat", - "color_strip.audio.viz.vu_meter": "VU Meter", - "color_strip.audio.viz.vu_meter.desc": "Volume level fills the strip", - "color_strip.audio.source": "Audio Source:", - "color_strip.audio.source.hint": "Audio source for this visualization. Can be a multichannel (device) or mono (single channel) source. Create and manage audio sources in the Sources tab.", - "color_strip.audio.sensitivity": "Sensitivity:", - "color_strip.audio.sensitivity.hint": "Gain multiplier for audio levels. Higher values make LEDs react to quieter sounds.", - "color_strip.audio.smoothing": "Smoothing:", - "color_strip.audio.smoothing.hint": "Temporal smoothing between frames. Higher values produce smoother but slower-reacting visuals.", - "color_strip.audio.palette": "Palette:", - "color_strip.audio.palette.hint": "Color palette used for spectrum bars or beat pulse coloring.", - "color_strip.audio.color": "Base Color:", - "color_strip.audio.color.hint": "Low-level color for VU meter bar.", - "color_strip.audio.color_peak": "Peak Color:", - "color_strip.audio.color_peak.hint": "High-level color at the top of the VU meter bar.", - "color_strip.audio.mirror": "Mirror:", - "color_strip.audio.mirror.hint": "Mirror spectrum from center outward: bass in the middle, treble at the edges.", - "color_strip.effect.type": "Effect Type:", - "color_strip.effect.type.hint": "Choose the procedural algorithm.", - "color_strip.effect.fire": "Fire", - "color_strip.effect.fire.desc": "Cellular automaton simulating rising flames with heat diffusion", - "color_strip.effect.meteor": "Meteor", - "color_strip.effect.meteor.desc": "Bright head travels along the strip with an exponential-decay tail", - "color_strip.effect.plasma": "Plasma", - "color_strip.effect.plasma.desc": "Overlapping sine waves mapped to a palette — classic demo-scene effect", - "color_strip.effect.noise": "Noise", - "color_strip.effect.noise.desc": "Scrolling fractal value noise mapped to a palette", - "color_strip.effect.aurora": "Aurora", - "color_strip.effect.aurora.desc": "Layered noise bands that drift and blend — northern lights style", - "color_strip.effect.rain": "Rain", - "color_strip.effect.rain.desc": "Raindrops fall down the strip with trailing tails", - "color_strip.effect.comet": "Comet", - "color_strip.effect.comet.desc": "Multiple comets with curved, pulsing tails", - "color_strip.effect.bouncing_ball": "Bouncing Ball", - "color_strip.effect.bouncing_ball.desc": "Physics-simulated balls bouncing with gravity", - "color_strip.effect.fireworks": "Fireworks", - "color_strip.effect.fireworks.desc": "Rockets launch and explode into colorful bursts", - "color_strip.effect.sparkle_rain": "Sparkle Rain", - "color_strip.effect.sparkle_rain.desc": "Twinkling star field with smooth fade-in/fade-out", - "color_strip.effect.lava_lamp": "Lava Lamp", - "color_strip.effect.lava_lamp.desc": "Slow-moving colored blobs that merge and separate", - "color_strip.effect.wave_interference": "Wave Interference", - "color_strip.effect.wave_interference.desc": "Two counter-propagating waves creating interference patterns", - "color_strip.effect.custom_palette": "Custom Palette:", - "color_strip.effect.custom_palette.hint": "JSON array of [position, R, G, B] stops, e.g. [[0,0,0,0],[0.5,255,0,0],[1,255,255,0]]", - "color_strip.effect.speed": "Speed:", - "color_strip.effect.speed.hint": "Speed multiplier for the effect animation (0.1 = very slow, 10.0 = very fast).", - "color_strip.effect.palette": "Palette:", - "color_strip.effect.palette.hint": "Color palette used to map effect values to RGB colors.", - "color_strip.effect.color": "Meteor Color:", - "color_strip.effect.color.hint": "Head color for the meteor effect.", - "color_strip.effect.intensity": "Intensity:", - "color_strip.effect.intensity.hint": "Effect intensity — controls spark rate (fire), tail decay (meteor), or brightness range (aurora).", - "color_strip.effect.scale": "Scale:", - "color_strip.effect.scale.hint": "Spatial scale — wave frequency (plasma), zoom level (noise), or band width (aurora).", - "color_strip.effect.mirror": "Mirror:", - "color_strip.effect.mirror.hint": "Bounce mode — the meteor reverses direction at strip ends instead of wrapping.", - "color_strip.palette.fire": "Fire", - "color_strip.palette.ocean": "Ocean", - "color_strip.palette.lava": "Lava", - "color_strip.palette.forest": "Forest", - "color_strip.palette.rainbow": "Rainbow", - "color_strip.palette.aurora": "Aurora", - "color_strip.palette.sunset": "Sunset", - "color_strip.palette.ice": "Ice", - "color_strip.palette.custom": "Custom", - "audio_source.title": "Audio Sources", - "audio_source.group.multichannel": "Multichannel", - "audio_source.group.mono": "Mono", - "audio_source.group.band_extract": "Band Extract", - "audio_source.add": "Add Audio Source", - "audio_source.add.multichannel": "Add Multichannel Source", - "audio_source.add.mono": "Add Mono Source", - "audio_source.add.band_extract": "Add Band Extract Source", - "audio_source.edit": "Edit Audio Source", - "audio_source.edit.multichannel": "Edit Multichannel Source", - "audio_source.edit.mono": "Edit Mono Source", - "audio_source.edit.band_extract": "Edit Band Extract Source", - "audio_source.name": "Name:", - "audio_source.name.placeholder": "System Audio", - "audio_source.name.hint": "A descriptive name for this audio source", - "audio_source.type": "Type:", - "audio_source.type.hint": "Multichannel captures all channels from a physical audio device. Mono extracts a single channel from a multichannel source.", - "audio_source.type.multichannel": "Multichannel", - "audio_source.type.mono": "Mono", - "audio_source.device": "Audio Device:", - "audio_source.device.hint": "Audio input source. Loopback devices capture system audio output; input devices capture microphone or line-in.", - "audio_source.refresh_devices": "Refresh devices", - "audio_source.parent": "Parent Source:", - "audio_source.parent.hint": "Multichannel source to extract a channel from", - "audio_source.channel": "Channel:", - "audio_source.channel.hint": "Which audio channel to extract from the multichannel source", - "audio_source.channel.mono": "Mono (L+R mix)", - "audio_source.channel.left": "Left", - "audio_source.channel.right": "Right", - "audio_source.description": "Description (optional):", - "audio_source.description.placeholder": "Describe this audio source...", - "audio_source.description.hint": "Optional notes about this audio source", - "audio_source.created": "Audio source created", - "audio_source.updated": "Audio source updated", - "audio_source.deleted": "Audio source deleted", - "audio_source.delete.confirm": "Are you sure you want to delete this audio source?", - "audio_source.error.name_required": "Please enter a name", - "audio_source.audio_template": "Audio Template:", - "audio_source.audio_template.hint": "Audio capture template that defines which engine and settings to use for this device", - "audio_source.band_parent": "Parent Audio Source:", - "audio_source.band_parent.hint": "Audio source to extract the frequency band from", - "audio_source.band": "Frequency Band:", - "audio_source.band.hint": "Select a frequency band preset or custom range", - "audio_source.band.bass": "Bass (20–250 Hz)", - "audio_source.band.mid": "Mid (250–4000 Hz)", - "audio_source.band.treble": "Treble (4000–20000 Hz)", - "audio_source.band.custom": "Custom Range", - "audio_source.freq_low": "Low Frequency (Hz):", - "audio_source.freq_high": "High Frequency (Hz):", - "audio_source.freq_range": "Frequency Range", - "audio_source.test": "Test", - "audio_source.test.title": "Test Audio Source", - "audio_source.test.rms": "RMS", - "audio_source.test.peak": "Peak", - "audio_source.test.beat": "Beat", - "audio_source.test.connecting": "Connecting...", - "audio_source.test.error": "Audio test failed", - "audio_template.test": "Test", - "audio_template.test.title": "Test Audio Template", - "audio_template.test.device": "Audio Device:", - "audio_template.test.device.hint": "Select which audio device to capture from during the test", - "audio_template.test.run": "Run", - "audio_template.title": "Audio Templates", - "audio_template.add": "Add Audio Template", - "audio_template.edit": "Edit Audio Template", - "audio_template.name": "Template Name:", - "audio_template.name.placeholder": "My Audio Template", - "audio_template.description.label": "Description (optional):", - "audio_template.description.placeholder": "Describe this template...", - "audio_template.engine": "Audio Engine:", - "audio_template.engine.hint": "Select the audio capture backend to use. WASAPI is Windows-only with loopback support. Sounddevice is cross-platform.", - "audio_template.engine.unavailable": "Unavailable", - "audio_template.engine.unavailable.hint": "This engine is not available on your system", - "audio_template.config": "Configuration", - "audio_template.config.show": "Show configuration", - "audio_template.created": "Audio template created", - "audio_template.updated": "Audio template updated", - "audio_template.deleted": "Audio template deleted", - "audio_template.delete.confirm": "Are you sure you want to delete this audio template?", - "audio_template.error.load": "Failed to load audio templates", - "audio_template.error.engines": "Failed to load audio engines", - "audio_template.error.required": "Please fill in all required fields", - "audio_template.error.delete": "Failed to delete audio template", - "streams.group.value": "Value Sources", - "streams.group.sync": "Sync Clocks", - "streams.group.gradients": "Gradients", - "gradient.group.title": "Gradients", - "gradient.add": "Add Gradient", - "gradient.edit": "Edit Gradient", - "gradient.builtin": "Built-in", - "gradient.stops_label": "stops", - "gradient.name": "Name:", - "gradient.name.hint": "A descriptive name for this gradient.", - "gradient.description": "Description:", - "gradient.description.hint": "Optional description for this gradient.", - "gradient.created": "Gradient created", - "gradient.updated": "Gradient updated", - "gradient.cloned": "Gradient cloned", - "gradient.deleted": "Gradient deleted", - "gradient.error.name_required": "Name is required", - "gradient.error.min_stops": "At least 2 color stops are required", - "gradient.error.delete_failed": "Failed to delete gradient", - "gradient.create_name": "New gradient name:", - "gradient.edit_name": "Rename gradient:", - "gradient.confirm_delete": "Delete gradient \"{name}\"?", - "section.empty.gradients": "No gradients yet", - "tree.group.picture": "Picture Source", - "tree.group.capture": "Screen Capture", - "tree.group.static": "Static", - "tree.group.processing": "Processed", - "tree.group.strip": "Color Strip", - "tree.group.audio": "Audio", - "tree.group.integrations": "Integrations", - "tree.group.utility": "Utility", - "tree.leaf.sources": "Sources", - "tree.leaf.engine_templates": "Engine Templates", - "tree.leaf.images": "Images", - "tree.leaf.video": "Video", - "tree.leaf.filter_templates": "Filter Templates", - "tree.leaf.processing_templates": "Processing Templates", - "tree.leaf.templates": "Templates", - "value_source.group.title": "Value Sources", - "value_source.select_type": "Select Value Source Type", - "value_source.add": "Add Value Source", - "value_source.edit": "Edit Value Source", - "value_source.name": "Name:", - "value_source.name.placeholder": "Brightness Pulse", - "value_source.name.hint": "A descriptive name for this value source", - "value_source.type": "Type:", - "value_source.type.hint": "Static outputs a constant value. Animated cycles through a waveform. Audio reacts to sound input. Adaptive types adjust brightness automatically based on time of day or scene content.", - "value_source.type.static": "Static", - "value_source.type.static.desc": "Constant output value", - "value_source.type.animated": "Animated", - "value_source.type.animated.desc": "Cycles through a waveform", - "value_source.type.audio": "Audio", - "value_source.type.audio.desc": "Reacts to sound input", - "value_source.type.adaptive_time": "Adaptive (Time)", - "value_source.type.adaptive_time.desc": "Adjusts by time of day", - "value_source.type.adaptive_scene": "Adaptive (Scene)", - "value_source.type.adaptive_scene.desc": "Adjusts by scene content", - "value_source.type.daylight": "Daylight Cycle", - "value_source.type.daylight.desc": "Brightness follows day/night cycle", - "value_source.daylight.speed": "Speed:", - "value_source.daylight.speed.hint": "Cycle speed multiplier. 1.0 = full day/night cycle in ~4 minutes. Higher values cycle faster.", - "value_source.daylight.use_real_time": "Use Real Time:", - "value_source.daylight.use_real_time.hint": "When enabled, brightness follows the actual time of day. Speed is ignored.", - "value_source.daylight.enable_real_time": "Follow wall clock", - "value_source.daylight.latitude": "Latitude:", - "value_source.daylight.latitude.hint": "Your geographic latitude (-90 to 90). Affects sunrise/sunset timing in real-time mode.", - "value_source.daylight.real_time": "Real-time", - "value_source.daylight.speed_label": "Speed", - "value_source.value": "Value:", - "value_source.value.hint": "Constant output value (0.0 = off, 1.0 = full brightness)", - "value_source.waveform": "Waveform:", - "value_source.waveform.hint": "Shape of the brightness animation cycle", - "value_source.waveform.sine": "Sine", - "value_source.waveform.triangle": "Triangle", - "value_source.waveform.square": "Square", - "value_source.waveform.sawtooth": "Sawtooth", - "value_source.speed": "Speed (cpm):", - "value_source.speed.hint": "Cycles per minute — how fast the waveform repeats (1 = very slow, 120 = very fast)", - "value_source.min_value": "Min Value:", - "value_source.min_value.hint": "Minimum output of the waveform cycle", - "value_source.max_value": "Max Value:", - "value_source.max_value.hint": "Maximum output of the waveform cycle", - "value_source.audio_source": "Audio Source:", - "value_source.audio_source.hint": "Audio source to read audio levels from (multichannel or mono)", - "value_source.mode": "Mode:", - "value_source.mode.hint": "RMS measures average volume. Peak tracks loudest moments. Beat triggers on rhythm.", - "value_source.mode.rms": "RMS (Volume)", - "value_source.mode.peak": "Peak", - "value_source.mode.beat": "Beat", - "value_source.mode.rms.desc": "Average volume level", - "value_source.mode.peak.desc": "Loudest moment tracking", - "value_source.mode.beat.desc": "Rhythm pulse detection", - "value_source.auto_gain": "Auto Gain:", - "value_source.auto_gain.hint": "Automatically normalize audio levels so output uses the full range, regardless of input volume", - "value_source.auto_gain.enable": "Enable auto-gain", - "value_source.sensitivity": "Sensitivity:", - "value_source.sensitivity.hint": "Gain multiplier for the audio signal (higher = more reactive)", - "value_source.scene_sensitivity.hint": "Gain multiplier for the luminance signal (higher = more reactive to brightness changes)", - "value_source.smoothing": "Smoothing:", - "value_source.smoothing.hint": "Temporal smoothing (0 = instant response, 1 = very smooth/slow)", - "value_source.audio_min_value": "Min Value:", - "value_source.audio_min_value.hint": "Output when audio is silent (e.g. 0.3 = 30% brightness floor)", - "value_source.audio_max_value": "Max Value:", - "value_source.audio_max_value.hint": "Output at maximum audio level", - "value_source.schedule": "Schedule:", - "value_source.schedule.hint": "Define at least 2 time points. Brightness interpolates linearly between them, wrapping at midnight.", - "value_source.schedule.add": "+ Add Point", - "value_source.schedule.points": "points", - "value_source.picture_source": "Picture Source:", - "value_source.picture_source.hint": "The picture source whose frames will be analyzed for average brightness.", - "value_source.scene_behavior": "Behavior:", - "value_source.scene_behavior.hint": "Complement: dark scene = high brightness (ideal for ambient backlight). Match: bright scene = high brightness.", - "value_source.scene_behavior.complement": "Complement (dark → bright)", - "value_source.scene_behavior.match": "Match (bright → bright)", - "value_source.adaptive_min_value": "Min Value:", - "value_source.adaptive_min_value.hint": "Minimum output brightness", - "value_source.adaptive_max_value": "Max Value:", - "value_source.adaptive_max_value.hint": "Maximum output brightness", - "value_source.error.schedule_min": "Schedule requires at least 2 time points", - "value_source.description": "Description (optional):", - "value_source.description.placeholder": "Describe this value source...", - "value_source.description.hint": "Optional notes about this value source", - "value_source.created": "Value source created", - "value_source.updated": "Value source updated", - "value_source.deleted": "Value source deleted", - "value_source.delete.confirm": "Are you sure you want to delete this value source?", - "value_source.error.name_required": "Please enter a name", - "value_source.test": "Test", - "value_source.test.title": "Test Value Source", - "value_source.test.connecting": "Connecting...", - "value_source.test.error": "Failed to connect", - "value_source.test.current": "Current", - "value_source.test.min": "Min", - "value_source.test.max": "Max", - "test.frames": "Frames", - "test.fps": "FPS", - "test.avg_capture": "Avg", - "targets.brightness_vs": "Brightness Source:", - "targets.brightness_vs.hint": "Optional value source that dynamically controls brightness each frame (overrides device brightness)", - "targets.brightness_vs.none": "None (device brightness)", - "targets.min_brightness_threshold": "Min Brightness Threshold:", - "targets.min_brightness_threshold.hint": "Effective output brightness (pixel brightness × device/source brightness) below this value turns LEDs off completely (0 = disabled)", - "targets.adaptive_fps": "Adaptive FPS:", - "targets.adaptive_fps.hint": "Automatically reduce send rate when the device becomes unresponsive, and gradually recover when it stabilizes. Recommended for WiFi devices with weak signal.", - "targets.protocol": "Protocol:", - "targets.protocol.hint": "DDP sends pixels via fast UDP (recommended for most setups). HTTP uses the JSON API — slower but reliable, limited to ~500 LEDs.", - "targets.protocol.ddp": "DDP (UDP)", - "targets.protocol.ddp.desc": "Fast raw UDP packets — recommended", - "targets.protocol.http": "HTTP", - "targets.protocol.http.desc": "JSON API — slower, ≤500 LEDs", - "targets.protocol.serial": "Serial", - "search.open": "Search (Ctrl+K)", - "search.placeholder": "Search entities... (Ctrl+K)", - "search.loading": "Loading...", - "search.no_results": "No results found", - "search.group.devices": "Devices", - "search.group.targets": "LED Targets", - "search.group.kc_targets": "Key Colors Targets", - "search.group.css": "Color Strip Sources", - "search.group.automations": "Automations", - "search.group.streams": "Picture Streams", - "search.group.capture_templates": "Capture Templates", - "search.group.pp_templates": "Post-Processing Templates", - "search.group.pattern_templates": "Pattern Templates", - "search.group.audio": "Audio Sources", - "search.group.value": "Value Sources", - "search.group.scenes": "Scene Presets", - "search.group.cspt": "Strip Processing Templates", - "search.group.sync_clocks": "Sync Clocks", - "search.group.actions": "Actions", - "search.action.start": "Start", - "search.action.stop": "Stop", - "search.action.activate": "Activate", - "search.action.enable": "Enable", - "search.action.disable": "Disable", - "settings.backup.label": "Backup Configuration", - "settings.backup.hint": "Download all configuration (devices, targets, streams, templates, automations) as a single JSON file.", - "settings.backup.button": "Download Backup", - "settings.backup.success": "Backup downloaded successfully", - "settings.backup.error": "Backup download failed", - "settings.restore.label": "Restore Configuration", - "settings.restore.hint": "Upload a previously downloaded backup file to replace all configuration. The server will restart automatically.", - "settings.restore.button": "Restore from Backup", - "settings.restore.confirm": "This will replace ALL configuration and restart the server. Are you sure?", - "settings.restore.success": "Configuration restored", - "settings.restore.error": "Restore failed", - "settings.restore.restarting": "Server is restarting...", - "settings.restore.restart_timeout": "Server did not respond. Please refresh the page manually.", - "settings.restart_server": "Restart Server", - "settings.restart_confirm": "Restart the server? Active targets will be stopped.", - "settings.restarting": "Restarting server...", - "settings.button.close": "Close", - "settings.log_level.label": "Log Level", - "settings.log_level.hint": "Change the server log verbosity at runtime. DEBUG shows the most detail; CRITICAL shows only fatal errors.", - "settings.log_level.save": "Apply", - "settings.log_level.saved": "Log level changed", - "settings.log_level.save_error": "Failed to change log level", - "settings.log_level.desc.debug": "Verbose developer output", - "settings.log_level.desc.info": "Normal operation messages", - "settings.log_level.desc.warning": "Potential problems", - "settings.log_level.desc.error": "Failures only", - "settings.log_level.desc.critical": "Fatal errors only", - "settings.auto_backup.label": "Auto-Backup", - "settings.auto_backup.hint": "Automatically create periodic backups of all configuration. Old backups are pruned when the maximum count is reached.", - "settings.auto_backup.enable": "Enable auto-backup", - "settings.auto_backup.interval_label": "Interval", - "settings.auto_backup.max_label": "Max backups", - "settings.auto_backup.save": "Save Settings", - "settings.auto_backup.saved": "Auto-backup settings saved", - "settings.auto_backup.save_error": "Failed to save auto-backup settings", - "settings.auto_backup.backup_now": "Backup Now", - "settings.auto_backup.backup_created": "Backup created", - "settings.auto_backup.backup_error": "Backup failed", - "settings.auto_backup.last_backup": "Last backup", - "settings.auto_backup.never": "Never", - "settings.saved_backups.label": "Saved Backups", - "settings.saved_backups.hint": "Auto-backup files stored on the server. Download to save locally, or delete to free space.", - "settings.saved_backups.empty": "No saved backups", - "settings.saved_backups.restore": "Restore", - "settings.saved_backups.download": "Download", - "settings.saved_backups.delete": "Delete", - "settings.saved_backups.delete_confirm": "Delete this backup file?", - "settings.saved_backups.delete_error": "Failed to delete backup", - "settings.saved_backups.type.auto": "auto", - "settings.saved_backups.type.manual": "manual", - "settings.mqtt.label": "MQTT", - "settings.mqtt.hint": "Configure MQTT broker connection for automation conditions and triggers.", - "settings.mqtt.enabled": "Enable MQTT", - "settings.mqtt.host_label": "Broker Host", - "settings.mqtt.port_label": "Port", - "settings.mqtt.username_label": "Username", - "settings.mqtt.password_label": "Password", - "settings.mqtt.password_set_hint": "Password is set — leave blank to keep", - "settings.mqtt.client_id_label": "Client ID", - "settings.mqtt.base_topic_label": "Base Topic", - "settings.mqtt.save": "Save MQTT Settings", - "settings.mqtt.saved": "MQTT settings saved", - "settings.mqtt.save_error": "Failed to save MQTT settings", - "settings.mqtt.error_host_required": "Broker host is required", - "settings.logs.label": "Server Logs", - "settings.logs.hint": "Stream live server log output. Use the filter to show only relevant log levels.", - "settings.logs.connect": "Connect", - "settings.logs.disconnect": "Disconnect", - "settings.logs.clear": "Clear", - "settings.logs.error": "Log viewer connection failed", - "settings.logs.filter.all": "All levels", - "settings.logs.filter.info": "Info+", - "settings.logs.filter.warning": "Warning+", - "settings.logs.filter.error": "Error only", - "settings.logs.filter.all_desc": "Show all log messages", - "settings.logs.filter.info_desc": "Info, warning, and errors", - "settings.logs.filter.warning_desc": "Warnings and errors only", - "settings.logs.filter.error_desc": "Errors only", - "device.error.power_off_failed": "Failed to turn off device", - "device.error.remove_failed": "Failed to remove device", - "device.error.settings_load_failed": "Failed to load device settings", - "device.error.brightness": "Failed to update brightness", - "device.error.required": "Please fill in all fields correctly", - "device.error.update": "Failed to update device", - "device.error.save": "Failed to save settings", - "device.error.clone_failed": "Failed to clone device", - "device_discovery.error.fill_all_fields": "Please fill in all fields", - "device_discovery.added": "Device added successfully", - "device_discovery.error.add_failed": "Failed to add device", - "calibration.error.load_failed": "Failed to load calibration", - "calibration.error.css_load_failed": "Failed to load color strip source", - "calibration.error.test_toggle_failed": "Failed to toggle test edge", - "calibration.error.save_failed": "Failed to save calibration", - "calibration.error.led_count_mismatch": "Total LEDs must equal the device LED count", - "calibration.error.led_count_exceeded": "Calibrated LEDs exceed the total LED count", - "calibration.mode.simple": "Simple", - "calibration.mode.advanced": "Advanced", - "calibration.switch_to_advanced": "Switch to Advanced", - "calibration.advanced.title": "Advanced Calibration", - "calibration.advanced.switch_to_simple": "Switch to Simple", - "calibration.advanced.lines_title": "Lines", - "calibration.advanced.canvas_hint": "Drag monitors to reposition. Click edges to select lines. Scroll to zoom, drag empty space to pan.", - "calibration.advanced.reset_view": "Reset view", - "calibration.advanced.line_properties": "Line Properties", - "calibration.advanced.picture_source": "Source:", - "calibration.advanced.picture_source.hint": "The picture source (monitor) this line samples from", - "calibration.advanced.edge": "Edge:", - "calibration.advanced.edge.hint": "Which screen edge to sample pixels from", - "calibration.advanced.led_count": "LEDs:", - "calibration.advanced.led_count.hint": "Number of LEDs mapped to this line", - "calibration.advanced.span_start": "Span Start:", - "calibration.advanced.span_start.hint": "Where sampling begins along the edge (0 = start, 1 = end). Use to cover only part of an edge.", - "calibration.advanced.span_end": "Span End:", - "calibration.advanced.span_end.hint": "Where sampling ends along the edge (0 = start, 1 = end). Together with Span Start, defines the active portion.", - "calibration.advanced.border_width": "Depth (px):", - "calibration.advanced.border_width.hint": "How many pixels deep from the edge to sample. Larger values capture more of the screen interior.", - "calibration.advanced.reverse": "Reverse", - "calibration.advanced.no_lines_warning": "Add at least one line", - "dashboard.error.automation_toggle_failed": "Failed to toggle automation", - "dashboard.error.start_failed": "Failed to start processing", - "dashboard.error.stop_failed": "Failed to stop processing", - "dashboard.error.stop_all": "Failed to stop all targets", - "target.error.editor_open_failed": "Failed to open target editor", - "target.error.start_failed": "Failed to start target", - "target.error.stop_failed": "Failed to stop target", - "target.error.clone_failed": "Failed to clone target", - "target.error.delete_failed": "Failed to delete target", - "targets.stop_all.button": "Stop All", - "targets.stop_all.none_running": "No targets are currently running", - "targets.stop_all.stopped": "Stopped {count} target(s)", - "targets.stop_all.error": "Failed to stop targets", - "audio_source.error.load": "Failed to load audio source", - "audio_template.error.clone_failed": "Failed to clone audio template", - "value_source.error.load": "Failed to load value source", - "color_strip.error.editor_open_failed": "Failed to open color strip editor", - "color_strip.error.clone_failed": "Failed to clone color strip source", - "color_strip.error.delete_failed": "Failed to delete color strip source", - "pattern.error.editor_open_failed": "Failed to open pattern template editor", - "pattern.error.clone_failed": "Failed to clone pattern template", - "pattern.error.delete_failed": "Failed to delete pattern template", - "pattern.error.capture_bg_failed": "Failed to capture background", - "stream.error.clone_picture_failed": "Failed to clone picture source", - "stream.error.clone_capture_failed": "Failed to clone capture template", - "stream.error.clone_pp_failed": "Failed to clone postprocessing template", - "kc_target.error.editor_open_failed": "Failed to open key colors editor", - "kc_target.error.clone_failed": "Failed to clone key colors target", - "kc_target.error.delete_failed": "Failed to delete key colors target", - "theme.switched.dark": "Switched to dark theme", - "theme.switched.light": "Switched to light theme", - "accent.color.updated": "Accent color updated", - "search.footer": "↑↓ navigate · Enter select · Esc close", - "sync_clock.group.title": "Sync Clocks", - "sync_clock.add": "Add Sync Clock", - "sync_clock.edit": "Edit Sync Clock", - "sync_clock.name": "Name:", - "sync_clock.name.placeholder": "Main Animation Clock", - "sync_clock.name.hint": "A descriptive name for this synchronization clock", - "sync_clock.speed": "Speed:", - "sync_clock.speed.hint": "Animation speed multiplier for all linked sources. 1.0 = normal, 2.0 = double speed, 0.5 = half speed.", - "sync_clock.description": "Description (optional):", - "sync_clock.description.placeholder": "Optional description", - "sync_clock.description.hint": "Optional notes about this clock's purpose", - "sync_clock.status.running": "Running", - "sync_clock.status.paused": "Paused", - "sync_clock.action.pause": "Pause", - "sync_clock.action.resume": "Resume", - "sync_clock.action.reset": "Reset", - "sync_clock.error.name_required": "Clock name is required", - "sync_clock.error.load": "Failed to load sync clock", - "sync_clock.created": "Sync clock created", - "sync_clock.updated": "Sync clock updated", - "sync_clock.deleted": "Sync clock deleted", - "sync_clock.paused": "Clock paused", - "sync_clock.resumed": "Clock resumed", - "sync_clock.reset_done": "Clock reset to zero", - "sync_clock.delete.confirm": "Delete this sync clock? Linked sources will lose synchronization and run at default speed.", - "sync_clock.elapsed": "Elapsed time", - "weather_source.group.title": "Weather Sources", - "weather_source.add": "Add Weather Source", - "weather_source.edit": "Edit Weather Source", - "weather_source.name": "Name:", - "weather_source.name.placeholder": "My Weather", - "weather_source.name.hint": "A descriptive name for this weather data source", - "weather_source.provider": "Provider:", - "weather_source.provider.hint": "Weather data provider. Open-Meteo is free and requires no API key.", - "weather_source.provider.open_meteo.desc": "Free, no API key required", - "weather_source.location": "Location:", - "weather_source.location.hint": "Your geographic coordinates. Use the auto-detect button or enter manually.", - "weather_source.latitude": "Lat:", - "weather_source.longitude": "Lon:", - "weather_source.use_my_location": "Use my location", - "weather_source.update_interval": "Update Interval:", - "weather_source.update_interval.hint": "How often to fetch weather data. Lower values give more responsive changes.", - "weather_source.description": "Description (optional):", - "weather_source.description.placeholder": "Optional description", - "weather_source.test": "Test", - "weather_source.error.name_required": "Weather source name is required", - "weather_source.error.load": "Failed to load weather source", - "weather_source.created": "Weather source created", - "weather_source.updated": "Weather source updated", - "weather_source.deleted": "Weather source deleted", - "weather_source.delete.confirm": "Delete this weather source? Linked color strip sources will lose weather data.", - "weather_source.geo.success": "Location detected", - "weather_source.geo.error": "Geolocation failed", - "weather_source.geo.not_supported": "Geolocation is not supported by your browser", - "streams.group.weather": "Weather", - "streams.group.home_assistant": "Home Assistant", - "ha_source.group.title": "Home Assistant Sources", - "ha_source.add": "Add Home Assistant Source", - "ha_source.edit": "Edit Home Assistant Source", - "ha_source.name": "Name:", - "ha_source.name.placeholder": "My Home Assistant", - "ha_source.name.hint": "A descriptive name for this Home Assistant connection", - "ha_source.host": "Host:", - "ha_source.host.hint": "Home Assistant host and port, e.g. 192.168.1.100:8123", - "ha_source.token": "Access Token:", - "ha_source.token.hint": "Long-Lived Access Token from HA (Profile > Security > Long-Lived Access Tokens)", - "ha_source.token.edit_hint": "Leave blank to keep the current token", - "ha_source.use_ssl": "Use SSL (wss://)", - "ha_source.entity_filters": "Entity Filters (optional):", - "ha_source.entity_filters.hint": "Comma-separated glob patterns, e.g. sensor.*, binary_sensor.front_door. Leave empty for all.", - "ha_source.description": "Description (optional):", - "ha_source.test": "Test Connection", - "ha_source.test.success": "Connected", - "ha_source.test.failed": "Connection failed", - "ha_source.connected": "Connected", - "ha_source.disconnected": "Disconnected", - "ha_source.error.name_required": "Name is required", - "ha_source.error.host_required": "Host is required", - "ha_source.error.token_required": "Access token is required", - "ha_source.error.load": "Failed to load Home Assistant source", - "ha_source.created": "Home Assistant source created", - "ha_source.updated": "Home Assistant source updated", - "ha_source.deleted": "Home Assistant source deleted", - "ha_source.delete.confirm": "Delete this Home Assistant connection?", - "section.empty.ha_sources": "No Home Assistant sources yet. Click + to add one.", - "ha_light.section.title": "Home Assistant", - "ha_light.section.targets": "Light Targets", - "ha_light.add": "Add HA Light Target", - "ha_light.edit": "Edit HA Light Target", - "ha_light.name": "Name:", - "ha_light.name.placeholder": "Living Room Lights", - "ha_light.ha_source": "HA Connection:", - "ha_light.css_source": "Color Strip Source:", - "ha_light.update_rate": "Update Rate:", - "ha_light.update_rate.hint": "How often to send color updates to HA lights (0.5-5.0 Hz). Lower values are safer for HA performance.", - "ha_light.transition": "Transition:", - "ha_light.transition.hint": "Smooth fade duration between colors (HA transition parameter).", - "ha_light.mappings": "Light Mappings:", - "ha_light.mappings.hint": "Map LED ranges to HA light entities. Each mapping averages the LED segment to a single color.", - "ha_light.mappings.add": "Add Mapping", - "ha_light.mapping.entity_id": "Entity ID:", - "ha_light.mapping.led_start": "LED Start:", - "ha_light.mapping.led_end": "LED End (-1=last):", - "ha_light.mapping.brightness": "Brightness Scale:", - "ha_light.description": "Description (optional):", - "ha_light.error.name_required": "Name is required", - "ha_light.error.ha_source_required": "HA connection is required", - "ha_light.created": "HA light target created", - "ha_light.updated": "HA light target updated", - "ha_light.mapping.select_entity": "Select a light entity...", - "ha_light.mapping.search_entity": "Search light entities...", - "section.empty.ha_light_targets": "No HA light targets yet. Click + to add one.", - "automations.condition.home_assistant": "Home Assistant", - "automations.condition.home_assistant.desc": "HA entity state", - "automations.condition.home_assistant.ha_source": "HA Source:", - "automations.condition.home_assistant.entity_id": "Entity ID:", - "automations.condition.home_assistant.state": "State:", - "automations.condition.home_assistant.match_mode": "Match Mode:", - "automations.condition.home_assistant.hint": "Activate when a Home Assistant entity matches the specified state", - "color_strip.clock": "Sync Clock:", - "color_strip.clock.hint": "Link to a sync clock to synchronize animation timing across sources. Speed is controlled on the clock.", - "graph.title": "Graph", - "graph.fit_all": "Fit all nodes", - "graph.zoom_in": "Zoom in", - "graph.zoom_out": "Zoom out", - "graph.search": "Search nodes", - "graph.search_placeholder": "Search entities...", - "graph.legend": "Legend", - "graph.minimap": "Minimap", - "graph.relayout": "Re-layout", - "graph.empty": "No entities yet", - "graph.empty.hint": "Create devices, sources, and targets to see them here.", - "graph.disconnect": "Disconnect", - "graph.connection_updated": "Connection updated", - "graph.connection_failed": "Failed to update connection", - "graph.connection_removed": "Connection removed", - "graph.disconnect_failed": "Failed to disconnect", - "graph.relayout_confirm": "Reset all manual node positions and re-layout the graph?", - "graph.fullscreen": "Toggle fullscreen", - "graph.add_entity": "Add entity", - "graph.color_picker": "Node color", - "graph.filter": "Filter nodes", - "graph.filter_placeholder": "Filter: name, type:x, tag:x", - "graph.filter_clear": "Clear filter", - "graph.filter_running": "Running", - "graph.filter_stopped": "Stopped", - "graph.filter_types": "Types", - "graph.filter_group.capture": "Capture", - "graph.filter_group.strip": "Color Strip", - "graph.filter_group.audio": "Audio", - "graph.filter_group.targets": "Targets", - "graph.filter_group.other": "Other", - "graph.bulk_delete_confirm": "Delete {count} selected entities?", - "graph.nothing_to_undo": "Nothing to undo", - "graph.nothing_to_redo": "Nothing to redo", - "graph.help_title": "Keyboard Shortcuts", - "graph.help.search": "Search", - "graph.help.filter": "Filter", - "graph.help.add": "Add entity", - "graph.help.shortcuts": "Shortcuts", - "graph.help.delete": "Delete / Detach", - "graph.help.select_all": "Select all", - "graph.help.undo": "Undo", - "graph.help.redo": "Redo", - "graph.help.fullscreen": "Fullscreen", - "graph.help.deselect": "Deselect", - "graph.help.navigate": "Navigate nodes", - "graph.help.click": "Click", - "graph.help.click_desc": "Select node", - "graph.help.dblclick": "Double-click", - "graph.help.dblclick_desc": "Zoom to node", - "graph.help.shift_click": "Shift+Click", - "graph.help.shift_click_desc": "Multi-select", - "graph.help.shift_drag": "Shift+Drag", - "graph.help.shift_drag_desc": "Rubber-band select", - "graph.help.drag_node": "Drag node", - "graph.help.drag_node_desc": "Reposition", - "graph.help.drag_port": "Drag port", - "graph.help.drag_port_desc": "Connect entities", - "graph.help.right_click": "Right-click edge", - "graph.help.right_click_desc": "Detach connection", - "graph.tooltip.fps": "FPS", - "graph.tooltip.errors": "Errors", - "graph.tooltip.uptime": "Uptime", - "automation.enabled": "Automation enabled", - "automation.disabled": "Automation disabled", - "scene_preset.activated": "Preset activated", - "scene_preset.used_by": "Used by %d automation(s)", - "settings.api_keys.label": "API Keys", - "settings.api_keys.hint": "API keys are defined in the server config file (config.yaml). Edit the file and restart the server to apply changes.", - "settings.api_keys.empty": "No API keys configured", - "settings.api_keys.load_error": "Failed to load API keys", - "settings.partial.label": "Partial Export / Import", - "settings.partial.hint": "Export or import a single entity type. Import replaces or merges existing data and restarts the server.", - "settings.partial.store.devices": "Devices", - "settings.partial.store.output_targets": "LED Targets", - "settings.partial.store.color_strip_sources": "Color Strips", - "settings.partial.store.picture_sources": "Picture Sources", - "settings.partial.store.audio_sources": "Audio Sources", - "settings.partial.store.audio_templates": "Audio Templates", - "settings.partial.store.capture_templates": "Capture Templates", - "settings.partial.store.postprocessing_templates": "Post-processing Templates", - "settings.partial.store.color_strip_processing_templates": "CSS Processing Templates", - "settings.partial.store.pattern_templates": "Pattern Templates", - "settings.partial.store.value_sources": "Value Sources", - "settings.partial.store.sync_clocks": "Sync Clocks", - "settings.partial.store.automations": "Automations", - "settings.partial.store.scene_presets": "Scene Presets", - "settings.partial.export_button": "Export", - "settings.partial.import_button": "Import from File", - "settings.partial.merge_label": "Merge (add/overwrite, keep existing)", - "settings.partial.export_success": "Exported successfully", - "settings.partial.export_error": "Export failed", - "settings.partial.import_success": "Imported successfully", - "settings.partial.import_error": "Import failed", - "settings.partial.import_confirm_replace": "This will REPLACE all {store} data and restart the server. Continue?", - "settings.partial.import_confirm_merge": "This will MERGE into existing {store} data and restart the server. Continue?", - "section.empty.devices": "No devices yet. Click + to add one.", - "section.empty.targets": "No LED targets yet. Click + to add one.", - "section.empty.kc_targets": "No key color targets yet. Click + to add one.", - "section.empty.pattern_templates": "No pattern templates yet. Click + to add one.", - "section.empty.picture_sources": "No sources yet. Click + to add one.", - "section.empty.capture_templates": "No capture templates yet. Click + to add one.", - "section.empty.pp_templates": "No post-processing templates yet. Click + to add one.", - "section.empty.audio_sources": "No audio sources yet. Click + to add one.", - "section.empty.audio_templates": "No audio templates yet. Click + to add one.", - "section.empty.color_strips": "No color strips yet. Click + to add one.", - "section.empty.value_sources": "No value sources yet. Click + to add one.", - "section.empty.sync_clocks": "No sync clocks yet. Click + to add one.", - "section.empty.weather_sources": "No weather sources yet. Click + to add one.", - "section.empty.cspt": "No CSS processing templates yet. Click + to add one.", - "section.empty.automations": "No automations yet. Click + to add one.", - "section.empty.scenes": "No scene presets yet. Click + to add one.", - "bulk.select": "Select", - "bulk.cancel": "Cancel", - "bulk.selected_count.one": "{count} selected", - "bulk.selected_count.other": "{count} selected", - "bulk.select_all": "Select all", - "bulk.deselect_all": "Deselect all", - "bulk.delete": "Delete", - "bulk.start": "Start", - "bulk.stop": "Stop", - "bulk.enable": "Enable", - "bulk.disable": "Disable", - "bulk.confirm_delete.one": "Delete {count} item?", - "bulk.confirm_delete.other": "Delete {count} items?", - "appearance.style.label": "Style Presets", - "appearance.style.hint": "Choose a visual theme — font pairing and color palette applied together.", - "appearance.preset.default": "Default", - "appearance.preset.midnight": "Midnight", - "appearance.preset.ember": "Ember", - "appearance.preset.arctic": "Arctic", - "appearance.preset.terminal": "Terminal", - "appearance.preset.neon": "Neon", - "appearance.preset.sakura": "Sakura", - "appearance.preset.ocean": "Ocean", - "appearance.preset.copper": "Copper", - "appearance.preset.vapor": "Vapor", - "appearance.preset.monolith": "Monolith", - "appearance.preset.applied": "Style preset applied", - "appearance.bg.label": "Background Effects", - "appearance.bg.hint": "Add an ambient background layer behind the interface.", - "appearance.bg.none": "None", - "appearance.bg.noise": "Noise Field", - "appearance.bg.aurora": "Aurora", - "appearance.bg.plasma": "Plasma", - "appearance.bg.rain": "Digital Rain", - "appearance.bg.stars": "Starfield", - "appearance.bg.warp": "Warp Tunnel", - "appearance.bg.grid": "Dot Grid", - "appearance.bg.mesh": "Gradient Mesh", - "appearance.bg.scanlines": "Scanlines", - "appearance.bg.applied": "Background effect applied", - - "settings.tab.updates": "Updates", - "settings.tab.about": "About", - "update.status_label": "Update Status", - "update.current_version": "Current version:", - "update.badge_tooltip": "New version available — click for details", - "update.available": "Version {version} is available", - "update.up_to_date": "You are running the latest version", - "update.prerelease": "pre-release", - "update.view_release": "View Release", - "update.dismiss": "Dismiss", - "update.check_now": "Check for Updates", - "update.check_error": "Update check failed", - "update.last_check": "Last check", - "update.never": "never", - "update.release_notes": "Release Notes", - "update.view_release_notes": "View Release Notes", - "update.auto_check_label": "Auto-Check Settings", - "update.auto_check_hint": "Periodically check for new releases in the background.", - "update.enable": "Enable auto-check", - "update.interval_label": "Check interval", - "update.channel_label": "Channel", - "update.channel.stable": "Stable", - "update.channel.stable_desc": "Stable releases only", - "update.channel.prerelease": "Pre-release", - "update.channel.prerelease_desc": "Include alpha, beta, and RC builds", - "update.save_settings": "Save Settings", - "update.settings_saved": "Update settings saved", - "update.settings_save_error": "Failed to save update settings", - "update.apply_now": "Update Now", - "update.apply_confirm": "Download and install version {version}? The server will restart automatically.", - "update.apply_error": "Update failed", - "update.applying": "Applying update…", - "update.downloading": "Downloading…", - "update.install_type_label": "Install type:", - "update.install_type.installer": "Windows installer", - "update.install_type.portable": "Portable", - "update.install_type.docker": "Docker", - "update.install_type.dev": "Development", - - "color_strip.notification.search_apps": "Search notification apps…", - - "asset.group.title": "Assets", - "asset.upload": "Upload Asset", - "asset.edit": "Edit Asset", - "asset.name": "Name:", - "asset.name.hint": "Display name for this asset.", - "asset.description": "Description:", - "asset.description.hint": "Optional description for this asset.", - "asset.file": "File:", - "asset.file.hint": "Select a file to upload (sound, image, video, or other).", - "asset.drop_or_browse": "Drop file here or click to browse", - "asset.uploaded": "Asset uploaded", - "asset.updated": "Asset updated", - "asset.deleted": "Asset deleted", - "asset.confirm_delete": "Delete this asset?", - "asset.error.name_required": "Name is required", - "asset.error.no_file": "Please select a file to upload", - "asset.error.delete_failed": "Failed to delete asset", - "asset.error.play_failed": "Failed to play sound", - "asset.error.download_failed": "Failed to download asset", - "asset.play": "Play", - "asset.download": "Download", - "asset.prebuilt": "Prebuilt", - "asset.prebuilt_restored": "{count} prebuilt asset(s) restored", - "asset.prebuilt_none_to_restore": "All prebuilt assets are already available", - "asset.restore_prebuilt": "Restore Prebuilt Sounds", - "asset.type.sound": "Sound", - "asset.type.image": "Image", - "asset.type.video": "Video", - "asset.type.other": "Other", - "streams.group.assets": "Assets", - "section.empty.assets": "No assets yet. Click + to upload one.", - - "donation.message": "LedGrab is free & open-source. If it's useful to you, consider supporting development.", - "donation.support": "Support the project", - "donation.view_source": "View source code", - "donation.later": "Remind me later", - "donation.dismiss": "Don't show again", - "donation.about_title": "About LedGrab", - "donation.about_opensource": "LedGrab is open-source software, free to use and modify.", - "donation.about_donate": "Support development", - "donation.about_license": "MIT License" -} \ No newline at end of file + "app.title": "LED Grab", + "app.version": "Version:", + "app.api_docs": "API Documentation", + "app.connection_lost": "Server unreachable", + "app.connection_retrying": "Attempting to reconnect…", + "app.server_restarting": "Server restarting…", + "app.server_restarting_sub": "Please wait, the server will be back shortly.", + "demo.badge": "DEMO", + "demo.banner": "You're in demo mode — all devices and data are virtual. No real hardware is used.", + "theme.toggle": "Toggle theme", + "bg.anim.toggle": "Toggle ambient background", + "accent.title": "Accent color", + "accent.custom": "Custom", + "accent.reset": "Reset", + "locale.change": "Change language", + "auth.login": "Login", + "auth.logout": "Logout", + "auth.authenticated": "● Authenticated", + "auth.title": "Login to LED Grab", + "auth.message": "Please enter your API key to authenticate and access the LED Grab.", + "auth.label": "API Key:", + "auth.placeholder": "Enter your API key...", + "auth.hint": "Your API key will be stored securely in your browser's local storage.", + "auth.button.cancel": "Cancel", + "auth.button.login": "Login", + "auth.error.required": "Please enter an API key", + "auth.success": "Logged in successfully!", + "auth.logout.confirm": "Are you sure you want to logout?", + "auth.logout.success": "Logged out successfully", + "auth.please_login": "Please login to view", + "auth.session_expired": "Your session has expired or the API key is invalid. Please login again.", + "auth.prompt_update": "Current API key is set. Enter new key to update or leave blank to remove:", + "auth.prompt_enter": "Enter your API key:", + "auth.toggle_password": "Toggle password visibility", + "api_key.login": "Login", + "displays.title": "Available Displays", + "displays.layout": "Displays", + "displays.information": "Display Information", + "displays.legend.primary": "Primary Display", + "displays.legend.secondary": "Secondary Display", + "displays.badge.primary": "Primary", + "displays.badge.secondary": "Secondary", + "displays.resolution": "Resolution:", + "displays.refresh_rate": "Refresh Rate:", + "displays.position": "Position:", + "displays.index": "Display Index:", + "displays.loading": "Loading displays...", + "displays.none": "No displays available", + "displays.failed": "Failed to load displays", + "displays.picker.title": "Select a Display", + "displays.picker.title.device": "Select a Device", + "displays.picker.select": "Select display...", + "displays.picker.click_to_select": "Click to select this display", + "displays.picker.adb_connect": "Connect ADB device", + "displays.picker.adb_connect.placeholder": "IP address (e.g. 192.168.2.201)", + "displays.picker.adb_connect.button": "Connect", + "displays.picker.adb_connect.success": "Device connected", + "displays.picker.adb_connect.error": "Failed to connect device", + "displays.picker.adb_disconnect": "Disconnect", + "displays.picker.no_android": "No Android devices found. Connect via USB or enter IP above.", + "templates.title": "Engine Templates", + "templates.description": "Capture templates define how the screen is captured. Each template uses a specific capture engine (MSS, DXcam, WGC) with custom settings. Assign templates to devices for optimal performance.", + "templates.loading": "Loading templates...", + "templates.empty": "No capture templates configured", + "templates.add": "Add Engine Template", + "templates.edit": "Edit Engine Template", + "templates.name": "Template Name:", + "templates.name.placeholder": "My Custom Template", + "templates.description.label": "Description (optional):", + "templates.description.placeholder": "Describe this template...", + "templates.engine": "Capture Engine:", + "templates.engine.hint": "Select the screen capture technology to use", + "templates.engine.select": "Select an engine...", + "templates.engine.unavailable": "Unavailable", + "templates.engine.unavailable.hint": "This engine is not available on your system", + "templates.engine.mss.desc": "Cross-platform, pure Python", + "templates.engine.dxcam.desc": "DirectX, low latency", + "templates.engine.bettercam.desc": "DirectX, high performance", + "templates.engine.camera.desc": "USB/IP camera capture", + "templates.engine.scrcpy.desc": "Android screen mirror", + "templates.engine.wgc.desc": "Windows Graphics Capture", + "templates.config": "Configuration", + "templates.config.show": "Show configuration", + "templates.config.none": "No additional configuration", + "templates.config.default": "Default", + "templates.config.camera_backend.auto": "Auto-detect best backend", + "templates.config.camera_backend.dshow": "Windows DirectShow", + "templates.config.camera_backend.msmf": "Windows Media Foundation", + "templates.config.camera_backend.v4l2": "Linux Video4Linux2", + "templates.created": "Template created successfully", + "templates.updated": "Template updated successfully", + "templates.deleted": "Template deleted successfully", + "templates.delete.confirm": "Are you sure you want to delete this template?", + "templates.error.load": "Failed to load templates", + "templates.error.engines": "Failed to load engines", + "templates.error.required": "Please fill in all required fields", + "templates.error.delete": "Failed to delete template", + "templates.test.title": "Test Capture", + "templates.test.description": "Test this template before saving to see a capture preview and performance metrics.", + "templates.test.display": "Display:", + "templates.test.display.select": "Select display...", + "templates.test.duration": "Capture Duration (s):", + "templates.test.border_width": "Border Width (px):", + "templates.test.run": "Run", + "templates.test.running": "Running test...", + "templates.test.results.preview": "Full Capture Preview", + "templates.test.results.borders": "Border Extraction", + "templates.test.results.top": "Top", + "templates.test.results.right": "Right", + "templates.test.results.bottom": "Bottom", + "templates.test.results.left": "Left", + "templates.test.results.performance": "Performance", + "templates.test.results.capture_time": "Capture", + "templates.test.results.extraction_time": "Extraction", + "templates.test.results.total_time": "Total", + "templates.test.results.max_fps": "Max FPS", + "templates.test.results.duration": "Duration", + "templates.test.results.frame_count": "Frames", + "templates.test.results.actual_fps": "Actual FPS", + "templates.test.results.avg_capture_time": "Avg Capture", + "templates.test.results.resolution": "Resolution:", + "templates.test.error.no_engine": "Please select a capture engine", + "templates.test.error.no_display": "Please select a display", + "templates.test.error.failed": "Test failed", + "devices.title": "Devices", + "device.select_type": "Select Device Type", + "devices.add": "Add New Device", + "devices.loading": "Loading devices...", + "devices.none": "No devices configured", + "devices.failed": "Failed to load devices", + "devices.wled_config": "WLED Configuration:", + "devices.wled_note": "Configure your WLED device (effects, segments, color order, power limits, etc.) using the", + "devices.wled_link": "official WLED app", + "devices.wled_note_or": "or the built-in", + "devices.wled_webui_link": "WLED Web UI", + "devices.wled_note_webui": "(open your device's IP in a browser).", + "devices.wled_note2": "This controller sends pixel color data and controls brightness per device.", + "device.scan": "Auto Discovery", + "device.scan.empty": "No devices found", + "device.scan.error": "Network scan failed", + "device.scan.already_added": "Already added", + "device.scan.selected": "Device selected", + "device.type": "Device Type:", + "device.type.hint": "Select the type of LED controller", + "device.type.wled": "WLED", + "device.type.wled.desc": "WiFi LED controller over HTTP/UDP", + "device.type.adalight": "Adalight", + "device.type.adalight.desc": "Serial LED protocol for Arduino", + "device.type.ambiled": "AmbiLED", + "device.type.ambiled.desc": "Serial protocol for AmbiLED devices", + "device.type.mqtt": "MQTT", + "device.type.mqtt.desc": "Publish LED data via MQTT broker", + "device.type.ws": "WebSocket", + "device.type.ws.desc": "Stream LED data to WebSocket clients", + "device.type.openrgb": "OpenRGB", + "device.type.openrgb.desc": "Control RGB peripherals via OpenRGB", + "device.type.dmx": "DMX", + "device.type.dmx.desc": "Art-Net / sACN (E1.31) stage lighting", + "device.type.mock": "Mock", + "device.type.mock.desc": "Virtual device for testing", + "device.type.espnow": "ESP-NOW", + "device.type.espnow.desc": "Ultra-low-latency via ESP32 gateway", + "device.type.hue": "Philips Hue", + "device.type.hue.desc": "Hue Entertainment API streaming", + "device.type.usbhid": "USB HID", + "device.type.usbhid.desc": "USB RGB peripherals (keyboards, mice)", + "device.type.spi": "SPI Direct", + "device.type.spi.desc": "Raspberry Pi GPIO/SPI LED strips", + "device.type.chroma": "Razer Chroma", + "device.type.chroma.desc": "Razer peripherals via Chroma SDK", + "device.type.gamesense": "SteelSeries", + "device.type.gamesense.desc": "SteelSeries peripherals via GameSense", + "device.chroma.device_type": "Peripheral Type:", + "device.chroma.device_type.hint": "Which Razer peripheral to control via Chroma SDK", + "device.gamesense.device_type": "Peripheral Type:", + "device.gamesense.device_type.hint": "Which SteelSeries peripheral to control via GameSense", + "device.espnow.peer_mac": "Peer MAC:", + "device.espnow.peer_mac.hint": "MAC address of the remote ESP32 receiver (e.g. AA:BB:CC:DD:EE:FF)", + "device.espnow.channel": "WiFi Channel:", + "device.espnow.channel.hint": "WiFi channel (1-14). Must match the receiver's channel.", + "device.hue.url": "Bridge IP:", + "device.hue.url.hint": "IP address of your Hue bridge", + "device.hue.username": "Bridge Username:", + "device.hue.username.hint": "Hue bridge application key from pairing", + "device.hue.client_key": "Client Key:", + "device.hue.client_key.hint": "Entertainment API client key (hex string from pairing)", + "device.hue.group_id": "Entertainment Group:", + "device.hue.group_id.hint": "Entertainment configuration ID from your Hue bridge", + "device.usbhid.url": "VID:PID:", + "device.usbhid.url.hint": "USB Vendor:Product ID in hex (e.g. 1532:0084)", + "device.spi.url": "GPIO/SPI Path:", + "device.spi.url.hint": "GPIO pin or SPI device path (e.g. spi://gpio:18)", + "device.spi.speed": "SPI Speed (Hz):", + "device.spi.speed.hint": "SPI clock speed. 800000 Hz for WS2812, 2400000 Hz for APA102.", + "device.spi.led_type": "LED Chipset:", + "device.spi.led_type.hint": "Type of addressable LED strip connected to the GPIO/SPI pin", + "device.spi.led_type.ws2812b.desc": "Most common, 800 KHz data, 3-wire RGB", + "device.spi.led_type.ws2812.desc": "Original WS2812, 800 KHz, 3-wire RGB", + "device.spi.led_type.ws2811.desc": "External driver IC, 400 KHz, 12V strips", + "device.spi.led_type.sk6812.desc": "Samsung LED, 800 KHz, 3-wire RGB", + "device.spi.led_type.sk6812_rgbw.desc": "SK6812 with dedicated white channel", + "device.gamesense.peripheral.keyboard": "Keyboard", + "device.gamesense.peripheral.keyboard.desc": "Per-key RGB illumination", + "device.gamesense.peripheral.mouse": "Mouse", + "device.gamesense.peripheral.mouse.desc": "Mouse RGB zones", + "device.gamesense.peripheral.headset": "Headset", + "device.gamesense.peripheral.headset.desc": "Headset earcup lighting", + "device.gamesense.peripheral.mousepad": "Mousepad", + "device.gamesense.peripheral.mousepad.desc": "Mousepad edge lighting zones", + "device.gamesense.peripheral.indicator": "Indicator", + "device.gamesense.peripheral.indicator.desc": "OLED/LED status indicator", + "device.css_processing_template": "Strip Processing Template:", + "device.css_processing_template.hint": "Default processing template applied to all color strip outputs on this device", + "device.dmx_protocol": "DMX Protocol:", + "device.dmx_protocol.hint": "Art-Net uses UDP port 6454, sACN (E1.31) uses UDP port 5568", + "device.dmx_protocol.artnet.desc": "UDP unicast, port 6454", + "device.dmx_protocol.sacn.desc": "Multicast/unicast, port 5568", + "device.dmx_start_universe": "Start Universe:", + "device.dmx_start_universe.hint": "First DMX universe (0-32767). Multiple universes are used automatically for >170 LEDs.", + "device.dmx_start_channel": "Start Channel:", + "device.dmx_start_channel.hint": "First DMX channel within the universe (1-512)", + "device.dmx.url": "IP Address:", + "device.dmx.url.hint": "IP address of the DMX node (e.g. 192.168.1.50)", + "device.dmx.url.placeholder": "192.168.1.50", + "device.serial_port": "Serial Port:", + "device.serial_port.hint": "Select the COM port of the Adalight device", + "device.serial_port.none": "No serial ports found", + "device.serial_port.select": "Select a port...", + "device.led_count_manual.hint": "Number of LEDs on the strip (must match your Arduino sketch)", + "device.baud_rate": "Baud Rate:", + "device.baud_rate.hint": "Serial communication speed. Higher = more FPS but requires matching Arduino sketch.", + "device.led_type": "LED Type:", + "device.led_type.hint": "RGB (3 channels) or RGBW (4 channels with dedicated white)", + "device.send_latency": "Send Latency (ms):", + "device.send_latency.hint": "Simulated network/serial delay per frame in milliseconds", + "device.mqtt_topic": "MQTT Topic:", + "device.mqtt_topic.hint": "MQTT topic path for publishing pixel data (e.g. mqtt://ledgrab/device/name)", + "device.mqtt_topic.placeholder": "mqtt://ledgrab/device/living-room", + "device.ws_url": "Connection URL:", + "device.ws_url.hint": "WebSocket URL for clients to connect and receive LED data", + "device.openrgb.url": "OpenRGB URL:", + "device.openrgb.url.hint": "OpenRGB server address (e.g. openrgb://localhost:6742/0)", + "device.openrgb.zone": "Zones:", + "device.openrgb.zone.hint": "Select which LED zones to control (leave all unchecked for all zones)", + "device.openrgb.zone.loading": "Loading zones…", + "device.openrgb.zone.error": "Failed to load zones", + "device.openrgb.mode": "Zone mode:", + "device.openrgb.mode.hint": "Combined treats all zones as one continuous LED strip. Separate renders each zone independently with the full effect.", + "device.openrgb.mode.combined": "Combined strip", + "device.openrgb.mode.separate": "Independent zones", + "device.openrgb.added_multiple": "Added {count} devices", + "device.url.hint": "IP address or hostname of the device (e.g. http://192.168.1.100)", + "device.name": "Device Name:", + "device.name.placeholder": "Living Room TV", + "device.url": "URL:", + "device.url.placeholder": "http://192.168.1.100", + "device.led_count": "LED Count:", + "device.led_count.hint": "Number of LEDs configured in the device", + "device.led_count.hint.auto": "Auto-detected from device", + "device.button.add": "Add Device", + "device.button.start": "Start", + "device.button.stop": "Stop", + "device.button.settings": "General Settings", + "device.button.capture_settings": "Capture Settings", + "device.button.calibrate": "Calibrate", + "device.button.remove": "Remove", + "device.button.webui": "Open Device Web UI", + "device.button.power_off": "Turn Off", + "device.button.ping": "Ping Device", + "device.ping.online": "Online ({ms}ms)", + "device.ping.offline": "Device offline", + "device.ping.error": "Ping failed", + "device.power.off_success": "Device turned off", + "device.status.connected": "Connected", + "device.status.disconnected": "Disconnected", + "device.status.error": "Error", + "device.status.processing": "Processing", + "device.status.idle": "Idle", + "device.fps": "FPS:", + "device.display": "Display:", + "device.remove.confirm": "Are you sure you want to remove this device?", + "device.added": "Device added successfully", + "device.removed": "Device removed", + "device.started": "Processing started", + "device.stopped": "Processing stopped", + "device.metrics.actual_fps": "Actual FPS", + "device.metrics.current_fps": "Current FPS", + "device.metrics.target_fps": "Target FPS", + "device.metrics.potential_fps": "Potential FPS", + "device.metrics.frames": "Frames", + "device.metrics.frames_skipped": "Skipped", + "device.metrics.keepalive": "Keepalive", + "device.metrics.errors": "Errors", + "device.metrics.uptime": "Uptime", + "device.metrics.timing": "Pipeline timing:", + "device.metrics.device_fps": "Device refresh rate", + "device.health.online": "Online", + "device.health.offline": "Offline", + "device.health.streaming_unreachable": "Unreachable during streaming", + "device.health.checking": "Checking...", + "device.last_seen.label": "Last seen", + "device.last_seen.just_now": "just now", + "device.last_seen.seconds": "%ds ago", + "device.last_seen.minutes": "%dm ago", + "device.last_seen.hours": "%dh ago", + "device.last_seen.days": "%dd ago", + "device.tutorial.start": "Start tutorial", + "device.tip.metadata": "Device info (LED count, type, color channels) is auto-detected from the device", + "device.tip.brightness": "Slide to adjust device brightness", + "device.tip.start": "Start or stop screen capture processing", + "device.tip.settings": "Configure general device settings (name, URL, health check)", + "device.tip.capture_settings": "Configure capture settings (display, capture template)", + "device.tip.calibrate": "Calibrate LED positions, direction, and coverage", + "device.tip.webui": "Open the device's built-in web interface for advanced configuration", + "device.tip.add": "Click here to add a new LED device", + "settings.title": "Settings", + "settings.tab.general": "General", + "settings.tab.backup": "Backup", + "settings.tab.mqtt": "MQTT", + "settings.tab.appearance": "Appearance", + "settings.logs.open_viewer": "Open Log Viewer", + "settings.external_url.label": "External URL", + "settings.external_url.hint": "If set, this base URL is used in webhook URLs and other user-visible links instead of the auto-detected local IP. Example: https://myserver.example.com:8080", + "settings.external_url.placeholder": "https://myserver.example.com:8080", + "settings.external_url.save": "Save", + "settings.external_url.saved": "External URL saved", + "settings.external_url.save_error": "Failed to save external URL", + "settings.general.title": "General Settings", + "settings.capture.title": "Capture Settings", + "settings.capture.saved": "Capture settings updated", + "settings.capture.failed": "Failed to save capture settings", + "settings.brightness": "Brightness:", + "settings.brightness.hint": "Global brightness for this device (0-100%)", + "settings.url.hint": "IP address or hostname of the device", + "settings.display_index": "Display:", + "settings.display_index.hint": "Which screen to capture for this device", + "settings.fps": "Target FPS:", + "settings.fps.hint": "Target frames per second (10-90)", + "settings.capture_template": "Engine Template:", + "settings.capture_template.hint": "Screen capture engine and configuration for this device", + "settings.button.cancel": "Cancel", + "settings.health_interval": "Health Check Interval (s):", + "settings.health_interval.hint": "How often to check the device status (5-600 seconds)", + "settings.auto_shutdown": "Auto Restore:", + "settings.auto_shutdown.hint": "Restore device to idle state when targets stop or server shuts down", + "settings.button.save": "Save Changes", + "settings.saved": "Settings saved successfully", + "settings.failed": "Failed to save settings", + "calibration.title": "LED Calibration", + "calibration.tip.led_count": "Enter LED count per edge", + "calibration.tip.start_corner": "Click a corner to set the start position", + "calibration.tip.direction": "Toggle LED strip direction (clockwise / counterclockwise)", + "calibration.tip.offset": "Set LED offset — distance from LED 0 to the start corner", + "calibration.tip.span": "Drag green bars to adjust coverage span", + "calibration.tip.test": "Click an edge to toggle test LEDs", + "calibration.tip.overlay": "Toggle screen overlay to see LED positions and numbering on your monitor", + "calibration.tip.toggle_inputs": "Click total LED count to toggle edge inputs", + "calibration.tip.border_width": "How many pixels from the screen edge to sample for LED colors", + "calibration.tip.skip_leds_start": "Skip LEDs at the start of the strip — skipped LEDs stay off", + "calibration.tip.skip_leds_end": "Skip LEDs at the end of the strip — skipped LEDs stay off", + "tour.welcome": "Welcome to LED Grab! This quick tour will show you around the interface. Use arrow keys or buttons to navigate.", + "tour.dashboard": "Dashboard — live overview of running targets, automations, and device health at a glance.", + "tour.targets": "Targets — add WLED devices, configure LED targets with capture settings and calibration.", + "tour.sources": "Sources — manage capture templates, picture sources, audio sources, and color strips.", + "tour.graph": "Graph — visual overview of all entities and their connections. Drag ports to connect, right-click edges to disconnect.", + "tour.automations": "Automations — automate scene switching with time, audio, or value conditions.", + "tour.settings": "Settings — backup and restore configuration, manage auto-backups.", + "tour.api": "API Docs — interactive REST API documentation powered by Swagger.", + "tour.search": "Search — quickly find and navigate to any entity with Ctrl+K.", + "tour.theme": "Theme — switch between dark and light mode.", + "tour.accent": "Accent color — customize the UI accent color to your preference.", + "tour.language": "Language — choose your preferred interface language.", + "tour.restart": "Restart tutorial", + "tour.dash.perf": "Performance — real-time FPS charts, latency metrics, and poll interval control.", + "tour.dash.running": "Running targets — live streaming metrics and quick stop control.", + "tour.dash.stopped": "Stopped targets — ready to start with one click.", + "tour.dash.automations": "Automations — active automation status and quick enable/disable toggle.", + "tour.tgt.led_tab": "LED tab — standard LED strip targets with device and color strip configuration.", + "tour.tgt.devices": "Devices — your LED controllers discovered on the network.", + "tour.tgt.css": "Color Strips — define how screen regions map to LED segments.", + "tour.tgt.targets": "LED Targets — combine a device, color strip, and capture source for streaming.", + "tour.tgt.kc_tab": "Key Colors — alternative target type using color-matching instead of pixel mapping.", + "tour.src.raw": "Raw — live screen capture sources from your displays.", + "tour.src.templates": "Capture Templates — reusable capture configurations (resolution, FPS, crop).", + "tour.src.static": "Static Image — test your setup with image files instead of live capture.", + "tour.src.processed": "Processed — apply post-processing effects like blur, brightness, or color correction.", + "tour.src.color_strip": "Color Strips — define how screen regions map to LED segments.", + "tour.src.audio": "Audio — analyze microphone or system audio for reactive LED effects.", + "tour.src.value": "Value — numeric data sources used as conditions in automations.", + "tour.src.sync": "Sync Clocks — shared timers that synchronize animations across multiple sources.", + "tour.auto.list": "Automations — automate scene activation based on time, audio, or value conditions.", + "tour.auto.add": "Click + to create a new automation with conditions and a scene to activate.", + "tour.auto.card": "Each card shows automation status, conditions, and quick controls to edit or toggle.", + "tour.auto.scenes_list": "Scenes — saved system states that automations can activate or you can apply manually.", + "tour.auto.scenes_add": "Click + to capture the current system state as a new scene preset.", + "tour.auto.scenes_card": "Each scene card shows target/device counts. Click to edit, recapture, or activate.", + "calibration.tutorial.start": "Start tutorial", + "calibration.overlay_toggle": "Overlay", + "calibration.start_position": "Starting Position:", + "calibration.position.bottom_left": "Bottom Left", + "calibration.position.bottom_right": "Bottom Right", + "calibration.position.top_left": "Top Left", + "calibration.position.top_right": "Top Right", + "calibration.direction": "Direction:", + "calibration.direction.clockwise": "Clockwise", + "calibration.direction.counterclockwise": "Counterclockwise", + "calibration.leds.top": "Top LEDs:", + "calibration.leds.right": "Right LEDs:", + "calibration.leds.bottom": "Bottom LEDs:", + "calibration.leds.left": "Left LEDs:", + "calibration.offset": "LED Offset:", + "calibration.offset.hint": "Distance from physical LED 0 to the start corner (along strip direction)", + "calibration.skip_start": "Skip LEDs (Start):", + "calibration.skip_start.hint": "Number of LEDs to turn off at the beginning of the strip (0 = none)", + "calibration.skip_end": "Skip LEDs (End):", + "calibration.skip_end.hint": "Number of LEDs to turn off at the end of the strip (0 = none)", + "calibration.border_width": "Border (px):", + "calibration.border_width.hint": "How many pixels from the screen edge to sample for LED colors (1-100)", + "calibration.button.cancel": "Cancel", + "calibration.button.save": "Save", + "calibration.saved": "Calibration saved", + "calibration.failed": "Failed to save calibration", + "server.healthy": "Server online", + "server.offline": "Server offline", + "error.unauthorized": "Unauthorized - please login", + "error.network": "Network error", + "error.unknown": "An error occurred", + "modal.discard_changes": "You have unsaved changes. Discard them?", + "confirm.title": "Confirm Action", + "confirm.yes": "Yes", + "confirm.no": "No", + "confirm.stop_all": "Stop all running targets?", + "confirm.turn_off_device": "Turn off this device?", + "common.loading": "Loading...", + "common.delete": "Delete", + "common.remove": "Remove", + "common.edit": "Edit", + "common.clone": "Clone", + "common.none": "None", + "common.none_no_cspt": "None (no processing template)", + "common.none_no_input": "None (no input source)", + "common.none_own_speed": "None (no sync)", + "common.undo": "Undo", + "validation.required": "This field is required", + "bulk.processing": "Processing…", + "api.error.timeout": "Request timed out — please try again", + "api.error.network": "Network error — check your connection", + "palette.search": "Search…", + "section.filter.placeholder": "Filter...", + "section.filter.reset": "Clear filter", + "tags.label": "Tags", + "tags.hint": "Assign tags for grouping and filtering cards", + "tags.placeholder": "Add tag...", + "section.expand_all": "Expand all sections", + "section.collapse_all": "Collapse all sections", + "streams.title": "Sources", + "streams.description": "Sources define the capture pipeline. A raw source captures from a display using a capture template. A processed source applies postprocessing to another source. Assign sources to devices.", + "streams.group.raw": "Sources", + "streams.group.raw_templates": "Engine Templates", + "streams.group.processed": "Sources", + "streams.group.proc_templates": "Filter Templates", + "streams.group.css_processing": "Processing Templates", + "streams.group.color_strip": "Color Strips", + "streams.group.audio": "Audio", + "streams.group.audio_templates": "Audio Templates", + "streams.section.streams": "Sources", + "streams.add": "Add Source", + "streams.add.raw": "Add Screen Capture", + "streams.add.processed": "Add Processed Source", + "streams.edit": "Edit Source", + "streams.edit.raw": "Edit Screen Capture", + "streams.edit.processed": "Edit Processed Source", + "streams.name": "Source Name:", + "streams.name.placeholder": "My Source", + "streams.type": "Type:", + "streams.type.raw": "Screen Capture", + "streams.type.processed": "Processed", + "streams.display": "Display:", + "streams.display.hint": "Which screen to capture", + "streams.capture_template": "Engine Template:", + "streams.capture_template.hint": "Engine template defining how the screen is captured", + "streams.target_fps": "Target FPS:", + "streams.target_fps.hint": "Target frames per second for capture (1-90)", + "streams.source": "Source:", + "streams.source.hint": "The source to apply processing filters to", + "streams.pp_template": "Filter Template:", + "streams.pp_template.hint": "Filter template to apply to the source", + "streams.description_label": "Description (optional):", + "streams.description_placeholder": "Describe this source...", + "streams.created": "Source created successfully", + "streams.updated": "Source updated successfully", + "streams.deleted": "Source deleted successfully", + "streams.delete.confirm": "Are you sure you want to delete this source?", + "streams.modal.loading": "Loading...", + "streams.error.load": "Failed to load sources", + "streams.error.required": "Please fill in all required fields", + "streams.error.delete": "Failed to delete source", + "streams.test.title": "Test Source", + "streams.test.run": "Run", + "streams.test.running": "Testing source...", + "streams.test.duration": "Capture Duration (s):", + "streams.test.error.failed": "Source test failed", + "postprocessing.title": "Filter Templates", + "postprocessing.description": "Processing templates define image filters and color correction. Assign them to processed picture sources for consistent postprocessing across devices.", + "postprocessing.add": "Add Filter Template", + "postprocessing.edit": "Edit Filter Template", + "postprocessing.name": "Template Name:", + "postprocessing.name.placeholder": "My Filter Template", + "filters.select_type": "Select filter type...", + "filters.add": "Add Filter", + "filters.remove": "Remove", + "filters.drag_to_reorder": "Drag to reorder", + "filters.empty": "No filters added. Use the selector below to add filters.", + "filters.brightness": "Brightness", + "filters.brightness.desc": "Adjust overall image brightness", + "filters.saturation": "Saturation", + "filters.saturation.desc": "Boost or reduce color intensity", + "filters.gamma": "Gamma", + "filters.gamma.desc": "Non-linear brightness curve correction", + "filters.downscaler": "Downscaler", + "filters.downscaler.desc": "Reduce resolution for faster processing", + "filters.pixelate": "Pixelate", + "filters.pixelate.desc": "Mosaic-style block averaging", + "filters.auto_crop": "Auto Crop", + "filters.auto_crop.desc": "Remove black bars from letterboxed content", + "filters.flip": "Flip", + "filters.flip.desc": "Mirror image horizontally or vertically", + "filters.color_correction": "Color Correction", + "filters.color_correction.desc": "White balance and color temperature", + "filters.filter_template": "Filter Template", + "filters.filter_template.desc": "Embed another processing template", + "filters.css_filter_template": "Strip Filter Template", + "filters.css_filter_template.desc": "Embed another strip processing template", + "filters.frame_interpolation": "Frame Interpolation", + "filters.frame_interpolation.desc": "Blend between frames for smoother output", + "filters.noise_gate": "Noise Gate", + "filters.noise_gate.desc": "Suppress small color changes below threshold", + "filters.palette_quantization": "Palette Quantization", + "filters.palette_quantization.desc": "Reduce colors to a limited palette", + "filters.reverse": "Reverse", + "filters.reverse.desc": "Reverse the LED order in the strip", + "postprocessing.description_label": "Description (optional):", + "postprocessing.description_placeholder": "Describe this template...", + "postprocessing.created": "Template created successfully", + "postprocessing.updated": "Template updated successfully", + "postprocessing.deleted": "Template deleted successfully", + "postprocessing.delete.confirm": "Are you sure you want to delete this filter template?", + "postprocessing.error.load": "Failed to load processing templates", + "postprocessing.error.required": "Please fill in all required fields", + "postprocessing.error.delete": "Failed to delete processing template", + "postprocessing.config.show": "Show settings", + "postprocessing.test.title": "Test Filter Template", + "postprocessing.test.source_stream": "Source:", + "postprocessing.test.running": "Testing processing template...", + "postprocessing.test.error.no_stream": "Please select a source", + "postprocessing.test.error.failed": "Processing template test failed", + "css_processing.title": "Strip Processing Templates", + "css_processing.add": "Add Strip Processing Template", + "css_processing.edit": "Edit Strip Processing Template", + "css_processing.name": "Template Name:", + "css_processing.name_placeholder": "My Strip Processing Template", + "css_processing.description_label": "Description (optional):", + "css_processing.description_placeholder": "Describe this template...", + "css_processing.created": "Strip processing template created", + "css_processing.updated": "Strip processing template updated", + "css_processing.deleted": "Strip processing template deleted", + "css_processing.delete.confirm": "Are you sure you want to delete this strip processing template?", + "css_processing.error.required": "Please fill in all required fields", + "css_processing.error.load": "Error loading strip processing template", + "css_processing.error.delete": "Error deleting strip processing template", + "css_processing.error.clone_failed": "Failed to clone strip processing template", + "device.button.stream_selector": "Source Settings", + "device.stream_settings.title": "Source Settings", + "device.stream_selector.label": "Source:", + "device.stream_selector.hint": "Select a source that defines what this device captures and processes", + "device.stream_selector.none": "-- No source assigned --", + "device.stream_selector.saved": "Source settings updated", + "device.stream_settings.border_width": "Border Width (px):", + "device.stream_settings.border_width_hint": "How many pixels from the screen edge to sample for LED colors (1-100)", + "device.stream_settings.interpolation": "Interpolation Mode:", + "device.stream_settings.interpolation.average": "Average", + "device.stream_settings.interpolation.median": "Median", + "device.stream_settings.interpolation.dominant": "Dominant", + "device.stream_settings.interpolation_hint": "How to calculate LED color from sampled pixels", + "device.stream_settings.smoothing": "Smoothing:", + "device.stream_settings.smoothing_hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.", + "device.tip.stream_selector": "Configure picture source and LED projection settings for this device", + "streams.group.static_image": "Static Image", + "streams.add.static_image": "Add Static Image Source", + "streams.edit.static_image": "Edit Static Image Source", + "streams.type.static_image": "Static Image", + "streams.group.video": "Video", + "streams.add.video": "Add Video Source", + "streams.edit.video": "Edit Video Source", + "picture_source.type.video": "Video", + "picture_source.type.video.desc": "Stream frames from an uploaded video asset", + "picture_source.video.loop": "Loop:", + "picture_source.video.speed": "Playback Speed:", + "picture_source.video.start_time": "Start Time (s):", + "picture_source.video.end_time": "End Time (s):", + "picture_source.video.resolution_limit": "Max Width (px):", + "picture_source.video.resolution_limit.hint": "Downscale video at decode time for performance", + "streams.image_asset": "Image Asset:", + "streams.image_asset.select": "Select image asset…", + "streams.image_asset.search": "Search image assets…", + "streams.video_asset": "Video Asset:", + "streams.video_asset.select": "Select video asset…", + "streams.video_asset.search": "Search video assets…", + "targets.title": "Targets", + "targets.description": "Targets bridge color strip sources to output devices. Each target references a device and a color strip source.", + "targets.subtab.wled": "LED", + "targets.subtab.led": "LED", + "targets.section.devices": "Devices", + "targets.section.color_strips": "Color Strip Sources", + "targets.section.targets": "Targets", + "targets.section.specific_settings": "Specific Settings", + "targets.section.advanced": "Advanced", + "targets.add": "Add Target", + "targets.edit": "Edit Target", + "targets.loading": "Loading targets...", + "targets.none": "No targets configured", + "targets.failed": "Failed to load targets", + "targets.name": "Target Name:", + "targets.name.placeholder": "My Target", + "targets.device": "Device:", + "targets.device.hint": "Select the LED device to send data to", + "targets.device.none": "-- Select a device --", + "targets.color_strip_source": "Color Strip Source:", + "targets.color_strip_source.hint": "Select the color strip source that provides LED colors for this target", + "targets.no_css": "No source", + "targets.source": "Source:", + "targets.source.hint": "Which picture source to capture and process", + "targets.source.none": "-- No source assigned --", + "targets.metrics.pipeline": "Pipeline details", + "targets.fps": "Target FPS:", + "targets.fps.hint": "Target frames per second for capture and LED updates (1-90)", + "targets.fps.rec": "Hardware max ≈ {fps} fps ({leds} LEDs)", + "targets.border_width": "Border Width (px):", + "targets.border_width.hint": "How many pixels from the screen edge to sample for LED colors (1-100)", + "targets.interpolation": "Interpolation Mode:", + "targets.interpolation.hint": "How to calculate LED color from sampled pixels", + "targets.interpolation.average": "Average", + "targets.interpolation.median": "Median", + "targets.interpolation.dominant": "Dominant", + "targets.smoothing": "Smoothing:", + "targets.smoothing.hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.", + "targets.keepalive_interval": "Keep Alive Interval:", + "targets.keepalive_interval.hint": "How often to resend the last frame when the source is static, keeping the device in live mode (0.5-5.0s)", + "targets.created": "Target created successfully", + "targets.updated": "Target updated successfully", + "targets.deleted": "Target deleted successfully", + "targets.delete.confirm": "Are you sure you want to delete this target?", + "targets.error.load": "Failed to load targets", + "targets.error.required": "Please fill in all required fields", + "targets.error.name_required": "Please enter a target name", + "targets.error.delete": "Failed to delete target", + "targets.button.start": "Start", + "targets.button.stop": "Stop", + "targets.status.processing": "Processing", + "targets.status.idle": "Idle", + "targets.status.error": "Error", + "targets.metrics.actual_fps": "Actual FPS", + "targets.metrics.target_fps": "Target FPS", + "targets.metrics.frames": "Frames", + "targets.metrics.errors": "Errors", + "targets.section.pattern_templates": "Pattern Templates", + "pattern.add": "Add Pattern Template", + "pattern.edit": "Edit Pattern Template", + "pattern.name": "Template Name:", + "pattern.name.placeholder": "My Pattern Template", + "pattern.description_label": "Description (optional):", + "pattern.description_placeholder": "Describe this pattern...", + "pattern.rectangles": "Rectangles", + "pattern.rect.name": "Name", + "pattern.rect.x": "X", + "pattern.rect.y": "Y", + "pattern.rect.width": "W", + "pattern.rect.height": "H", + "pattern.rect.add": "Add Rectangle", + "pattern.rect.remove": "Remove", + "pattern.rect.empty": "No rectangles defined. Add at least one rectangle.", + "pattern.created": "Pattern template created successfully", + "pattern.updated": "Pattern template updated successfully", + "pattern.deleted": "Pattern template deleted successfully", + "pattern.delete.confirm": "Are you sure you want to delete this pattern template?", + "pattern.delete.referenced": "Cannot delete: this template is referenced by a target", + "pattern.error.required": "Please fill in all required fields", + "pattern.visual_editor": "Visual Editor", + "pattern.capture_bg": "Capture Background", + "pattern.source_for_bg": "Source for Background:", + "pattern.source_for_bg.none": "-- Select source --", + "pattern.delete_selected": "Delete Selected", + "pattern.name.hint": "A descriptive name for this rectangle layout", + "pattern.description.hint": "Optional notes about where or how this pattern is used", + "pattern.visual_editor.hint": "Click + buttons to add rectangles. Drag edges to resize, drag inside to move.", + "pattern.rectangles.hint": "Fine-tune rectangle positions and sizes with exact coordinates (0.0 to 1.0)", + "overlay.toggle": "Toggle screen overlay", + "overlay.button.show": "Show overlay visualization", + "overlay.button.hide": "Hide overlay visualization", + "overlay.started": "Overlay visualization started", + "overlay.stopped": "Overlay visualization stopped", + "overlay.error.start": "Failed to start overlay", + "overlay.error.stop": "Failed to stop overlay", + "dashboard.title": "Dashboard", + "dashboard.section.targets": "Targets", + "dashboard.section.running": "Running", + "dashboard.section.stopped": "Stopped", + "dashboard.no_targets": "No targets configured", + "dashboard.uptime": "Uptime", + "dashboard.fps": "FPS", + "dashboard.errors": "Errors", + "dashboard.device": "Device", + "dashboard.stop_all": "Stop All", + "dashboard.failed": "Failed to load dashboard", + "dashboard.section.automations": "Automations", + "dashboard.section.scenes": "Scene Presets", + "dashboard.section.sync_clocks": "Sync Clocks", + "dashboard.targets": "Targets", + "dashboard.section.performance": "System Performance", + "dashboard.perf.cpu": "CPU", + "dashboard.perf.ram": "RAM", + "dashboard.perf.gpu": "GPU", + "dashboard.perf.unavailable": "unavailable", + "dashboard.perf.color": "Chart color", + "dashboard.perf.mode.system": "System", + "dashboard.perf.mode.app": "App", + "dashboard.perf.mode.both": "Both", + "dashboard.poll_interval": "Refresh interval", + "automations.title": "Automations", + "automations.empty": "No automations configured. Create one to automate scene activation.", + "automations.add": "Add Automation", + "automations.edit": "Edit Automation", + "automations.delete.confirm": "Delete automation \"{name}\"?", + "automations.name": "Name:", + "automations.name.hint": "A descriptive name for this automation", + "automations.name.placeholder": "My Automation", + "automations.enabled": "Enabled:", + "automations.enabled.hint": "Disabled automations won't activate even when conditions are met", + "automations.condition_logic": "Condition Logic:", + "automations.condition_logic.hint": "How multiple conditions are combined: ANY (OR) or ALL (AND)", + "automations.condition_logic.or": "Any condition (OR)", + "automations.condition_logic.and": "All conditions (AND)", + "automations.condition_logic.or.desc": "Triggers when any condition matches", + "automations.condition_logic.and.desc": "Triggers only when all match", + "automations.conditions": "Conditions:", + "automations.conditions.hint": "Rules that determine when this automation activates", + "automations.conditions.add": "Add Condition", + "automations.conditions.empty": "No conditions — automation is always active when enabled", + "automations.condition.always": "Always", + "automations.condition.always.desc": "Always active", + "automations.condition.always.hint": "Automation activates immediately when enabled and stays active.", + "automations.condition.startup": "Startup", + "automations.condition.startup.desc": "On server start", + "automations.condition.startup.hint": "Activates when the server starts and stays active while enabled.", + "automations.condition.application": "Application", + "automations.condition.application.desc": "App running/focused", + "automations.condition.application.apps": "Applications:", + "automations.condition.application.apps.hint": "Process names, one per line (e.g. firefox.exe)", + "automations.condition.application.browse": "Browse", + "automations.condition.application.search": "Filter processes...", + "automations.condition.application.no_processes": "No processes found", + "automations.condition.application.match_type": "Match Type:", + "automations.condition.application.match_type.hint": "How to detect the application", + "automations.condition.application.match_type.running": "Running", + "automations.condition.application.match_type.running.desc": "Process is active", + "automations.condition.application.match_type.topmost": "Topmost", + "automations.condition.application.match_type.topmost.desc": "Foreground window", + "automations.condition.application.match_type.topmost_fullscreen": "Topmost + FS", + "automations.condition.application.match_type.topmost_fullscreen.desc": "Foreground + fullscreen", + "automations.condition.application.match_type.fullscreen": "Fullscreen", + "automations.condition.application.match_type.fullscreen.desc": "Any fullscreen app", + "automations.condition.time_of_day": "Time of Day", + "automations.condition.time_of_day.desc": "Time range", + "automations.condition.time_of_day.start_time": "Start Time:", + "automations.condition.time_of_day.end_time": "End Time:", + "automations.condition.time_of_day.overnight_hint": "For overnight ranges (e.g. 22:00–06:00), set start time after end time.", + "automations.condition.system_idle": "System Idle", + "automations.condition.system_idle.desc": "User idle/active", + "automations.condition.system_idle.idle_minutes": "Idle Timeout (minutes):", + "automations.condition.system_idle.mode": "Trigger Mode:", + "automations.condition.system_idle.when_idle": "When idle", + "automations.condition.system_idle.when_active": "When active", + "automations.condition.display_state": "Display State", + "automations.condition.display_state.desc": "Monitor on/off", + "automations.condition.display_state.state": "Monitor State:", + "automations.condition.display_state.on": "On", + "automations.condition.display_state.off": "Off (sleeping)", + "automations.condition.mqtt": "MQTT", + "automations.condition.mqtt.desc": "MQTT message", + "automations.condition.mqtt.topic": "Topic:", + "automations.condition.mqtt.payload": "Payload:", + "automations.condition.mqtt.match_mode": "Match Mode:", + "automations.condition.mqtt.match_mode.exact": "Exact", + "automations.condition.mqtt.match_mode.contains": "Contains", + "automations.condition.mqtt.match_mode.regex": "Regex", + "automations.condition.mqtt.hint": "Activate when an MQTT topic receives a matching payload", + "automations.condition.webhook": "Webhook", + "automations.condition.webhook.desc": "HTTP callback", + "automations.condition.webhook.hint": "Activate via an HTTP call from external services (Home Assistant, IFTTT, curl, etc.)", + "automations.condition.webhook.url": "Webhook URL:", + "automations.condition.webhook.copy": "Copy", + "automations.condition.webhook.copied": "Copied!", + "automations.condition.webhook.save_first": "Save the automation first to generate a webhook URL", + "automations.scene": "Scene:", + "automations.scene.hint": "Scene preset to activate when conditions are met", + "automations.scene.search_placeholder": "Search scenes...", + "automations.scene.none_selected": "None (no scene)", + "automations.scene.none_available": "No scenes available", + "automations.deactivation_mode": "Deactivation:", + "automations.deactivation_mode.hint": "What happens when conditions stop matching", + "automations.deactivation_mode.none": "None", + "automations.deactivation_mode.none.desc": "Keep current state", + "automations.deactivation_mode.revert": "Revert", + "automations.deactivation_mode.revert.desc": "Restore previous state", + "automations.deactivation_mode.fallback_scene": "Fallback", + "automations.deactivation_mode.fallback_scene.desc": "Activate a fallback scene", + "automations.deactivation_scene": "Fallback Scene:", + "automations.deactivation_scene.hint": "Scene to activate when this automation deactivates", + "automations.status.active": "Active", + "automations.status.inactive": "Inactive", + "automations.status.disabled": "Disabled", + "automations.action.disable": "Disable", + "automations.last_activated": "Last activated", + "automations.logic.and": " AND ", + "automations.logic.or": " OR ", + "automations.logic.all": "ALL", + "automations.logic.any": "ANY", + "automations.updated": "Automation updated", + "automations.created": "Automation created", + "automations.deleted": "Automation deleted", + "automations.error.name_required": "Name is required", + "automations.error.clone_failed": "Failed to clone automation", + "scenes.title": "Scenes", + "scenes.add": "Capture Scene", + "scenes.edit": "Edit Scene", + "scenes.name": "Name:", + "scenes.name.hint": "A descriptive name for this scene preset", + "scenes.name.placeholder": "My Scene", + "scenes.description": "Description:", + "scenes.description.hint": "Optional description of what this scene does", + "scenes.targets": "Targets:", + "scenes.targets.hint": "Select which targets to include in this scene snapshot", + "scenes.targets.add": "Add Target", + "scenes.targets.search_placeholder": "Search targets...", + "scenes.capture": "Capture", + "scenes.activate": "Activate scene", + "scenes.recapture": "Recapture current state", + "scenes.delete": "Delete scene", + "scenes.targets_count": "targets", + "scenes.captured": "Scene captured", + "scenes.updated": "Scene updated", + "scenes.activated": "Scene activated", + "scenes.activated_partial": "Scene partially activated", + "scenes.errors": "errors", + "scenes.recaptured": "Scene recaptured", + "scenes.deleted": "Scene deleted", + "scenes.recapture_confirm": "Recapture current state into \"{name}\"?", + "scenes.delete_confirm": "Delete scene \"{name}\"?", + "scenes.error.name_required": "Name is required", + "scenes.error.save_failed": "Failed to save scene", + "scenes.error.activate_failed": "Failed to activate scene", + "scenes.error.recapture_failed": "Failed to recapture scene", + "scenes.error.delete_failed": "Failed to delete scene", + "scenes.cloned": "Scene cloned", + "scenes.error.clone_failed": "Failed to clone scene", + "time.hours_minutes": "{h}h {m}m", + "time.minutes_seconds": "{m}m {s}s", + "time.seconds": "{s}s", + "dashboard.type.led": "LED", + "dashboard.type.kc": "Key Colors", + "aria.close": "Close", + "aria.save": "Save", + "aria.cancel": "Cancel", + "aria.previous": "Previous", + "aria.next": "Next", + "aria.hint": "Show hint", + "color_strip.select_type": "Select Color Strip Type", + "color_strip.add": "Add", + "color_strip.edit": "Edit", + "color_strip.name": "Name:", + "color_strip.name.placeholder": "Wall Strip", + "color_strip.picture_source": "Picture Source:", + "color_strip.picture_source.hint": "Which screen capture source to use as input for LED color calculation", + "color_strip.fps": "Target FPS:", + "color_strip.fps.hint": "Target frames per second for LED color updates (10-90)", + "color_strip.interpolation": "Color Mode:", + "color_strip.interpolation.hint": "How to calculate LED color from sampled border pixels", + "color_strip.interpolation.average": "Average", + "color_strip.interpolation.median": "Median", + "color_strip.interpolation.dominant": "Dominant", + "color_strip.interpolation.average.desc": "Blend all sampled pixels into a smooth color", + "color_strip.interpolation.median.desc": "Pick the middle color, reducing outliers", + "color_strip.interpolation.dominant.desc": "Use the most frequent color in the sample", + "color_strip.smoothing": "Smoothing:", + "color_strip.smoothing.hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.", + "color_strip.frame_interpolation": "Frame Interpolation:", + "color_strip.frame_interpolation.hint": "Blends between consecutive captured frames to produce output at the full target FPS even when capture rate is lower. Reduces visible stepping on slow ambient transitions.", + "color_strip.color_corrections": "Color Corrections", + "color_strip.brightness": "Brightness:", + "color_strip.brightness.hint": "Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.", + "color_strip.saturation": "Saturation:", + "color_strip.saturation.hint": "Color saturation (0=grayscale, 1=unchanged, 2=double saturation)", + "color_strip.gamma": "Gamma:", + "color_strip.gamma.hint": "Gamma correction (1=none, <1=brighter midtones, >1=darker midtones)", + "color_strip.test_device": "Test on Device:", + "color_strip.test_device.hint": "Select a device to send test pixels to when clicking edge toggles", + "color_strip.leds": "LED count", + "color_strip.led_count": "LED Count:", + "color_strip.led_count.hint": "Total number of LEDs on the physical strip. For screen sources: 0 = auto from calibration (extra LEDs not mapped to edges will be black). For static color: set to match your device LED count.", + "color_strip.created": "Color strip source created", + "color_strip.updated": "Color strip source updated", + "color_strip.deleted": "Color strip source deleted", + "color_strip.delete.confirm": "Are you sure you want to delete this color strip source?", + "color_strip.delete.referenced": "Cannot delete: this source is in use by a target", + "color_strip.error.name_required": "Please enter a name", + "color_strip.type": "Type:", + "color_strip.type.hint": "Picture Source derives LED colors from a screen capture. Static Color fills all LEDs with a single constant color. Gradient distributes a color gradient across all LEDs. Color Cycle smoothly cycles through a user-defined list of colors. Composite stacks multiple sources as blended layers. Audio Reactive drives LEDs from real-time audio input. API Input receives raw LED colors from external clients via REST or WebSocket.", + "color_strip.type.picture": "Picture Source", + "color_strip.type.picture.desc": "Colors from screen capture", + "color_strip.type.picture_advanced": "Multi-Monitor", + "color_strip.type.picture_advanced.desc": "Line-based calibration across monitors", + "color_strip.type.static": "Static Color", + "color_strip.type.static.desc": "Single solid color fill", + "color_strip.type.gradient": "Gradient", + "color_strip.type.gradient.desc": "Smooth color transition across LEDs", + "color_strip.type.color_cycle": "Color Cycle", + "color_strip.type.color_cycle.desc": "Cycle through a list of colors", + "color_strip.static_color": "Color:", + "color_strip.static_color.hint": "The solid color that will be sent to all LEDs on the strip.", + "color_strip.gradient.preview": "Gradient:", + "color_strip.gradient.preview.hint": "Visual preview. Click the marker track below to add a stop. Drag markers to reposition.", + "color_strip.gradient.easing": "Easing:", + "color_strip.gradient.easing.hint": "Controls how colors blend between gradient stops.", + "color_strip.gradient.easing.linear": "Linear", + "color_strip.gradient.easing.linear.desc": "Constant-rate blending between stops", + "color_strip.gradient.easing.ease_in_out": "Smooth", + "color_strip.gradient.easing.ease_in_out.desc": "S-curve: slow start and end, fast middle", + "color_strip.gradient.easing.step": "Step", + "color_strip.gradient.easing.step.desc": "Hard jumps between colors with no blending", + "color_strip.gradient.easing.cubic": "Cubic", + "color_strip.gradient.easing.cubic.desc": "Cubic ease — accelerating blend curve", + "color_strip.gradient.stops": "Color Stops:", + "color_strip.gradient.stops.hint": "Each stop defines a color at a relative position (0.0 = start, 1.0 = end). The ↔ button adds a right-side color to create a hard edge at that stop.", + "color_strip.gradient.stops_count": "stops", + "color_strip.gradient.add_stop": "+ Add Stop", + "color_strip.gradient.position": "Position (0.0–1.0)", + "color_strip.gradient.bidir.hint": "Add a second color on the right side of this stop to create a hard edge in the gradient.", + "color_strip.gradient.min_stops": "Gradient must have at least 2 stops", + "color_strip.gradient.select": "Gradient:", + "color_strip.gradient.select.hint": "Select a gradient from the library. Create and edit gradients in the Gradients tab.", + "color_strip.gradient.error.no_gradient": "Please select a gradient", + "color_strip.gradient.preset": "Preset:", + "color_strip.gradient.preset.hint": "Load a predefined gradient palette. Selecting a preset replaces the current stops.", + "color_strip.gradient.preset.custom": "— Custom —", + "color_strip.gradient.preset.rainbow": "Rainbow", + "color_strip.gradient.preset.sunset": "Sunset", + "color_strip.gradient.preset.ocean": "Ocean", + "color_strip.gradient.preset.forest": "Forest", + "color_strip.gradient.preset.fire": "Fire", + "color_strip.gradient.preset.lava": "Lava", + "color_strip.gradient.preset.aurora": "Aurora", + "color_strip.gradient.preset.ice": "Ice", + "color_strip.gradient.preset.warm": "Warm", + "color_strip.gradient.preset.cool": "Cool", + "color_strip.gradient.preset.neon": "Neon", + "color_strip.gradient.preset.pastel": "Pastel", + "color_strip.gradient.preset.save_button": "Save as preset…", + "color_strip.gradient.preset.save_prompt": "Enter a name for this preset:", + "color_strip.gradient.preset.saved": "Preset saved", + "color_strip.gradient.preset.deleted": "Preset deleted", + "color_strip.gradient.preset.apply": "Apply", + "color_strip.animation": "Animation", + "color_strip.animation.type": "Effect:", + "color_strip.animation.type.hint": "Animation effect to apply.", + "color_strip.animation.type.none": "None (no animation effect)", + "color_strip.animation.type.none.desc": "Static colors with no animation", + "color_strip.animation.type.breathing": "Breathing", + "color_strip.animation.type.breathing.desc": "Smooth brightness fade in and out", + "color_strip.animation.type.color_cycle": "Color Cycle", + "color_strip.animation.type.gradient_shift": "Gradient Shift", + "color_strip.animation.type.gradient_shift.desc": "Slides the gradient along the strip", + "color_strip.animation.type.wave": "Wave", + "color_strip.animation.type.wave.desc": "Sinusoidal brightness wave moving along the strip", + "color_strip.animation.type.strobe": "Strobe", + "color_strip.animation.type.strobe.desc": "Rapid on/off flashing", + "color_strip.animation.type.sparkle": "Sparkle", + "color_strip.animation.type.sparkle.desc": "Random LEDs flash briefly", + "color_strip.animation.type.pulse": "Pulse", + "color_strip.animation.type.pulse.desc": "Sharp brightness pulse with quick fade", + "color_strip.animation.type.candle": "Candle", + "color_strip.animation.type.candle.desc": "Warm flickering candle-like glow", + "color_strip.animation.type.rainbow_fade": "Rainbow Fade", + "color_strip.animation.type.rainbow_fade.desc": "Cycles through the entire hue spectrum", + "color_strip.animation.type.noise_perturb": "Noise Perturb", + "color_strip.animation.type.noise_perturb.desc": "Perturbs gradient stop positions with organic noise each frame", + "color_strip.animation.type.hue_rotate": "Hue Rotate", + "color_strip.animation.type.hue_rotate.desc": "Smoothly rotates all pixel hues while preserving saturation and brightness", + "color_strip.animation.speed": "Speed:", + "color_strip.animation.speed.hint": "Animation speed multiplier. 1.0 ≈ one cycle per second for Breathing; higher values cycle faster.", + "color_strip.color_cycle.colors": "Colors:", + "color_strip.color_cycle.colors.hint": "List of colors to cycle through smoothly. At least 2 required. Default is a full rainbow spectrum.", + "color_strip.color_cycle.add_color": "+ Add Color", + "color_strip.color_cycle.speed": "Speed:", + "color_strip.color_cycle.speed.hint": "Cycle speed multiplier. 1.0 ≈ one full cycle every 20 seconds; higher values cycle faster.", + "color_strip.color_cycle.min_colors": "Color cycle must have at least 2 colors", + "color_strip.type.effect": "Effect", + "color_strip.type.effect.desc": "Procedural effects like fire, plasma, aurora", + "color_strip.type.effect.hint": "Procedural LED effects (fire, meteor, plasma, noise, aurora) generated in real time.", + "color_strip.type.composite": "Composite", + "color_strip.type.composite.desc": "Stack and blend multiple sources", + "color_strip.type.composite.hint": "Stack multiple color strip sources as layers with blend modes and opacity.", + "color_strip.type.mapped": "Mapped", + "color_strip.type.mapped.desc": "Assign sources to LED zones", + "color_strip.type.mapped.hint": "Assign different color strip sources to different LED ranges (zones). Unlike composite which blends layers, mapped places sources side-by-side.", + "color_strip.type.audio": "Audio Reactive", + "color_strip.type.audio.desc": "LEDs driven by audio input", + "color_strip.type.audio.hint": "LED colors driven by real-time audio input — system audio or microphone.", + "color_strip.type.api_input": "API Input", + "color_strip.type.api_input.desc": "Receive colors from external apps", + "color_strip.type.api_input.hint": "Receives raw LED color arrays from external clients via REST POST or WebSocket. Use this to integrate with custom software, home automation, or any system that can send HTTP requests.", + "color_strip.api_input.fallback_color": "Fallback Color:", + "color_strip.api_input.fallback_color.hint": "Color to display when no data has been received within the timeout period. LEDs will show this color on startup and after the connection is lost.", + "color_strip.api_input.timeout": "Timeout (seconds):", + "color_strip.api_input.timeout.hint": "How long to wait for new color data before reverting to the fallback color. Set to 0 to never time out.", + "color_strip.api_input.endpoints": "Push Endpoints:", + "color_strip.api_input.endpoints.hint": "Use these URLs to push LED color data from your external application. REST accepts JSON, WebSocket accepts both JSON and raw binary frames.", + "color_strip.api_input.save_first": "Save the source first to see the push endpoint URLs.", + "color_strip.api_input.interpolation": "LED Interpolation:", + "color_strip.api_input.interpolation.hint": "How to resize incoming LED data when its count differs from the device's LED count. Linear gives smooth blending, Nearest preserves sharp edges, None truncates or zero-pads.", + "color_strip.api_input.interpolation.linear": "Linear", + "color_strip.api_input.interpolation.linear.desc": "Smooth blending between LEDs", + "color_strip.api_input.interpolation.nearest": "Nearest", + "color_strip.api_input.interpolation.nearest.desc": "Sharp edges, no blending", + "color_strip.api_input.interpolation.none": "None", + "color_strip.api_input.interpolation.none.desc": "Truncate or zero-pad", + "color_strip.type.notification": "Notification", + "color_strip.type.notification.desc": "One-shot effect on webhook trigger", + "color_strip.type.notification.hint": "Fires a one-shot visual effect (flash, pulse, sweep) when triggered via a webhook. Designed for use as a composite layer over a persistent base source.", + "color_strip.notification.os_listener": "Listen to OS Notifications:", + "color_strip.notification.os_listener.hint": "When enabled, this source automatically fires when a desktop notification appears (Windows toast / Linux D-Bus). Requires the app to have notification access permission.", + "color_strip.notification.effect": "Effect:", + "color_strip.notification.effect.hint": "Visual effect when a notification fires. Flash fades linearly, Pulse uses a smooth bell curve, Sweep fills LEDs left-to-right then fades.", + "color_strip.notification.effect.flash": "Flash", + "color_strip.notification.effect.flash.desc": "Instant on, linear fade-out", + "color_strip.notification.effect.pulse": "Pulse", + "color_strip.notification.effect.pulse.desc": "Smooth bell-curve glow", + "color_strip.notification.effect.sweep": "Sweep", + "color_strip.notification.effect.sweep.desc": "Fills left-to-right then fades", + "color_strip.notification.effect.chase": "Chase", + "color_strip.notification.effect.chase.desc": "Light travels across strip and bounces back", + "color_strip.notification.effect.gradient_flash": "Gradient Flash", + "color_strip.notification.effect.gradient_flash.desc": "Bright center fades to edges, then all fades out", + "color_strip.notification.duration": "Duration (ms):", + "color_strip.notification.duration.hint": "How long the notification effect plays, in milliseconds.", + "color_strip.notification.default_color": "Default Color:", + "color_strip.notification.default_color.hint": "Color used when the notification has no app-specific color mapping.", + "color_strip.notification.filter_mode": "App Filter:", + "color_strip.notification.filter_mode.hint": "Filter notifications by app name. Off = accept all, Whitelist = only listed apps, Blacklist = all except listed apps.", + "color_strip.notification.filter_mode.off": "Off", + "color_strip.notification.filter_mode.whitelist": "Whitelist", + "color_strip.notification.filter_mode.blacklist": "Blacklist", + "color_strip.notification.filter_mode.off.desc": "Accept all notifications", + "color_strip.notification.filter_mode.whitelist.desc": "Only listed apps", + "color_strip.notification.filter_mode.blacklist.desc": "All except listed apps", + "color_strip.notification.filter_list": "App List:", + "color_strip.notification.filter_list.hint": "One app name per line. Use Browse to pick from running processes.", + "color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram", + "color_strip.notification.app_colors": "App Colors", + "color_strip.notification.app_colors.label": "Color Mappings:", + "color_strip.notification.app_colors.hint": "Per-app color overrides. Each row maps an app name to a specific notification color.", + "color_strip.notification.app_colors.add": "+ Add Mapping", + "color_strip.notification.app_overrides": "Per-App Overrides", + "color_strip.notification.app_overrides.label": "App Overrides:", + "color_strip.notification.app_overrides.hint": "Per-app overrides for color and sound. Each row can set a custom color, sound asset, and volume for a specific app.", + "color_strip.notification.app_overrides.add": "+ Add Override", + "color_strip.notification.app_overrides.app_placeholder": "App name", + "color_strip.notification.sound": "Sound", + "color_strip.notification.sound.asset": "Sound Asset:", + "color_strip.notification.sound.asset.hint": "Pick a sound asset to play when a notification fires. Leave empty for silent.", + "color_strip.notification.sound.none": "None (silent)", + "color_strip.notification.sound.search": "Search sounds…", + "color_strip.notification.sound.volume": "Volume:", + "color_strip.notification.sound.volume.hint": "Global volume for notification sounds (0–100%).", + "color_strip.notification.sound.app_sounds": "Per-App Sounds:", + "color_strip.notification.sound.app_sounds.hint": "Override sound and volume for specific apps. Empty sound = mute that app.", + "color_strip.notification.sound.app_sounds.add": "+ Add Override", + "color_strip.notification.sound.app_name_placeholder": "App name", + "color_strip.notification.endpoint": "Webhook Endpoint:", + "color_strip.notification.endpoint.hint": "Use this URL to trigger notifications from external systems. POST with optional JSON body: {\"app\": \"AppName\", \"color\": \"#FF0000\"}.", + "color_strip.notification.save_first": "Save the source first to see the webhook endpoint URL.", + "color_strip.notification.app_count": "apps", + "color_strip.notification.test": "Test notification", + "color_strip.notification.test.ok": "Notification sent", + "color_strip.notification.test.no_streams": "No running streams for this source", + "color_strip.notification.test.error": "Failed to send notification", + "color_strip.notification.history.title": "Notification History", + "color_strip.notification.history.hint": "Recent OS notifications captured by the listener (newest first). Up to 50 entries.", + "color_strip.notification.history.empty": "No notifications captured yet", + "color_strip.notification.history.unavailable": "OS notification listener is not available on this platform", + "color_strip.notification.history.error": "Failed to load notification history", + "color_strip.notification.history.refresh": "Refresh", + "color_strip.notification.history.unknown_app": "Unknown app", + "color_strip.notification.history.fired": "Streams triggered", + "color_strip.notification.history.filtered": "Streams filtered", + "color_strip.test.title": "Test Preview", + "color_strip.test.connecting": "Connecting...", + "color_strip.test.error": "Failed to connect to preview stream", + "color_strip.test.led_count": "LEDs:", + "color_strip.test.fps": "FPS:", + "color_strip.test.receive_fps": "Receive FPS", + "color_strip.test.apply": "Apply", + "color_strip.test.composite": "Composite", + "color_strip.preview.title": "Live Preview", + "color_strip.preview.not_connected": "Not connected", + "color_strip.preview.connecting": "Connecting...", + "color_strip.preview.connected": "Connected", + "color_strip.preview.unsupported": "Preview not available for this source type", + "color_strip.type.daylight": "Daylight Cycle", + "color_strip.type.daylight.desc": "Simulates natural daylight over 24 hours", + "color_strip.type.daylight.hint": "Simulates the sun's color temperature throughout a 24-hour day/night cycle — from warm sunrise to cool daylight to warm sunset and dim night.", + "color_strip.daylight.speed": "Speed:", + "color_strip.daylight.speed.hint": "Cycle speed multiplier. 1.0 = full day/night cycle in ~4 minutes. Higher values cycle faster.", + "color_strip.daylight.use_real_time": "Use Real Time:", + "color_strip.daylight.use_real_time.hint": "When enabled, LED color matches the actual time of day on this computer. Speed setting is ignored.", + "color_strip.daylight.real_time": "Real Time", + "color_strip.daylight.latitude": "Latitude:", + "color_strip.daylight.latitude.hint": "Your geographic latitude (-90 to 90). Affects sunrise/sunset timing in real-time mode.", + "color_strip.daylight.longitude": "Longitude:", + "color_strip.daylight.longitude.hint": "Your geographic longitude (-180 to 180). Adjusts solar noon offset for accurate sunrise/sunset timing.", + "color_strip.type.candlelight": "Candlelight", + "color_strip.type.candlelight.desc": "Realistic flickering candle simulation", + "color_strip.type.candlelight.hint": "Simulates realistic candle flickering across all LEDs with warm tones and organic flicker patterns.", + "color_strip.type.key_colors": "Key Colors", + "color_strip.type.key_colors.desc": "Extract colors from screen regions", + "color_strip.key_colors.picture_source": "Picture Source:", + "color_strip.key_colors.interpolation": "Color Mode:", + "color_strip.key_colors.smoothing": "Smoothing:", + "color_strip.key_colors.brightness": "Brightness:", + "color_strip.key_colors.rectangles": "Screen Regions:", + "color_strip.key_colors.no_rects": "No regions defined. Click Configure Regions to add.", + "color_strip.key_colors.configure_regions": "Configure Regions", + "color_strip.key_colors.mode.average": "Average", + "color_strip.key_colors.mode.average.desc": "Mean color of all pixels in the region", + "color_strip.key_colors.mode.median": "Median", + "color_strip.key_colors.mode.median.desc": "Median color (less affected by outliers)", + "color_strip.key_colors.mode.dominant": "Dominant", + "color_strip.key_colors.mode.dominant.desc": "Most frequent color (K-means clustering)", + "color_strip.key_colors.error.no_source": "Picture source is required", + "color_strip.key_colors.error.no_rects": "At least one screen region is required", + "color_strip.type.weather": "Weather", + "color_strip.type.weather.desc": "Weather-reactive ambient colors", + "color_strip.type.weather.hint": "Maps real-time weather conditions to ambient LED colors. Requires a Weather Source entity.", + "color_strip.weather.source": "Weather Source:", + "color_strip.weather.source.hint": "The weather data source to use for ambient colors. Create one in the Weather tab first.", + "color_strip.weather.speed": "Animation Speed:", + "color_strip.weather.speed.hint": "Speed of the ambient color drift animation. Higher = faster movement.", + "color_strip.weather.temperature_influence": "Temperature Influence:", + "color_strip.weather.temperature_influence.hint": "How much the current temperature shifts the palette warm/cool. 0 = pure condition colors, 1 = strong shift.", + "color_strip.weather.error.no_source": "Please select a weather source", + "color_strip.candlelight.color": "Base Color:", + "color_strip.candlelight.color.hint": "The warm base color of the candle flame. Default is a natural warm amber.", + "color_strip.candlelight.intensity": "Flicker Intensity:", + "color_strip.candlelight.intensity.hint": "How much the candles flicker. Low values produce a gentle glow, high values simulate a windy candle.", + "color_strip.candlelight.num_candles_label": "Number of Candles:", + "color_strip.candlelight.num_candles": "candles", + "color_strip.candlelight.num_candles.hint": "How many independent candle sources along the strip. Each flickers with its own pattern.", + "color_strip.candlelight.speed": "Flicker Speed:", + "color_strip.candlelight.speed.hint": "Speed of the flicker animation. Higher values produce faster, more restless flames.", + "color_strip.candlelight.wind": "Wind:", + "color_strip.candlelight.wind.hint": "Wind simulation strength. Higher values create correlated gusts that make all candles flicker together.", + "color_strip.candlelight.type": "Candle Type:", + "color_strip.candlelight.type.hint": "Preset that adjusts flicker behavior without changing other settings.", + "color_strip.candlelight.type.default": "Default", + "color_strip.candlelight.type.default.desc": "Standard candle flicker", + "color_strip.candlelight.type.taper": "Taper", + "color_strip.candlelight.type.taper.desc": "Tall, steady candle with reduced flicker", + "color_strip.candlelight.type.votive": "Votive", + "color_strip.candlelight.type.votive.desc": "Small, flickery candle with narrow glow", + "color_strip.candlelight.type.bonfire": "Bonfire", + "color_strip.candlelight.type.bonfire.desc": "Large, chaotic fire with extra warmth", + "color_strip.type.processed": "Processed", + "color_strip.type.processed.desc": "Apply a processing template to another source", + "color_strip.type.processed.hint": "Wraps an existing color strip source and pipes its output through a filter chain.", + "color_strip.processed.input": "Source:", + "color_strip.processed.input.hint": "The color strip source whose output will be processed", + "color_strip.processed.template": "Processing Template:", + "color_strip.processed.template.hint": "Filter chain to apply to the input source output", + "color_strip.processed.error.no_input": "Please select an input source", + "color_strip.composite.layers": "Layers:", + "color_strip.composite.layers.hint": "Stack multiple color strip sources. First layer is the bottom, last is the top. Each layer can have its own blend mode and opacity.", + "color_strip.composite.add_layer": "+ Add Layer", + "color_strip.composite.source": "Source", + "color_strip.composite.blend_mode": "Blend", + "color_strip.composite.blend_mode.normal": "Normal", + "color_strip.composite.blend_mode.normal.desc": "Standard alpha blending", + "color_strip.composite.blend_mode.add": "Add", + "color_strip.composite.blend_mode.add.desc": "Brightens by adding colors", + "color_strip.composite.blend_mode.multiply": "Multiply", + "color_strip.composite.blend_mode.multiply.desc": "Darkens by multiplying colors", + "color_strip.composite.blend_mode.screen": "Screen", + "color_strip.composite.blend_mode.screen.desc": "Brightens, inverse of multiply", + "color_strip.composite.blend_mode.override": "Override", + "color_strip.composite.blend_mode.overlay": "Overlay", + "color_strip.composite.blend_mode.overlay.desc": "Multiply darks, screen lights", + "color_strip.composite.blend_mode.soft_light": "Soft Light", + "color_strip.composite.blend_mode.soft_light.desc": "Gentle contrast adjustment", + "color_strip.composite.blend_mode.hard_light": "Hard Light", + "color_strip.composite.blend_mode.hard_light.desc": "Strong contrast, vivid colors", + "color_strip.composite.blend_mode.difference": "Difference", + "color_strip.composite.blend_mode.difference.desc": "Absolute color difference", + "color_strip.composite.blend_mode.exclusion": "Exclusion", + "color_strip.composite.blend_mode.exclusion.desc": "Like difference, lower contrast", + "color_strip.composite.blend_mode.override.desc": "Black = transparent, bright = opaque", + "color_strip.composite.opacity": "Opacity", + "color_strip.composite.brightness": "Brightness", + "color_strip.composite.brightness.none": "None (full brightness)", + "color_strip.composite.processing": "Processing", + "color_strip.composite.enabled": "Enabled", + "color_strip.composite.error.min_layers": "At least 1 layer is required", + "color_strip.composite.error.no_source": "Each layer must have a source selected", + "color_strip.composite.layers_count": "layers", + "color_strip.composite.range": "LED Range", + "color_strip.composite.range_start": "Start", + "color_strip.composite.range_end": "End", + "color_strip.composite.reverse": "Reverse", + "color_strip.mapped.zones": "Zones:", + "color_strip.mapped.zones.hint": "Each zone maps a color strip source to a specific LED range. Zones are placed side-by-side — gaps between zones stay black.", + "color_strip.mapped.add_zone": "+ Add Zone", + "color_strip.mapped.zone_source": "Source", + "color_strip.mapped.zone_start": "Start LED", + "color_strip.mapped.zone_end": "End LED", + "color_strip.mapped.zone_reverse": "Reverse", + "color_strip.mapped.zones_count": "zones", + "color_strip.mapped.select_source": "Search sources...", + "color_strip.mapped.error.no_source": "Each zone must have a source selected", + "color_strip.audio.visualization": "Visualization:", + "color_strip.audio.visualization.hint": "How audio data is rendered to LEDs.", + "color_strip.audio.viz.spectrum": "Spectrum Analyzer", + "color_strip.audio.viz.spectrum.desc": "Frequency bars across the strip", + "color_strip.audio.viz.beat_pulse": "Beat Pulse", + "color_strip.audio.viz.beat_pulse.desc": "All LEDs pulse on the beat", + "color_strip.audio.viz.vu_meter": "VU Meter", + "color_strip.audio.viz.vu_meter.desc": "Volume level fills the strip", + "color_strip.audio.source": "Audio Source:", + "color_strip.audio.source.hint": "Audio source for this visualization. Can be a multichannel (device) or mono (single channel) source. Create and manage audio sources in the Sources tab.", + "color_strip.audio.sensitivity": "Sensitivity:", + "color_strip.audio.sensitivity.hint": "Gain multiplier for audio levels. Higher values make LEDs react to quieter sounds.", + "color_strip.audio.smoothing": "Smoothing:", + "color_strip.audio.smoothing.hint": "Temporal smoothing between frames. Higher values produce smoother but slower-reacting visuals.", + "color_strip.audio.palette": "Palette:", + "color_strip.audio.palette.hint": "Color palette used for spectrum bars or beat pulse coloring.", + "color_strip.audio.color": "Base Color:", + "color_strip.audio.color.hint": "Low-level color for VU meter bar.", + "color_strip.audio.color_peak": "Peak Color:", + "color_strip.audio.color_peak.hint": "High-level color at the top of the VU meter bar.", + "color_strip.audio.mirror": "Mirror:", + "color_strip.audio.mirror.hint": "Mirror spectrum from center outward: bass in the middle, treble at the edges.", + "color_strip.effect.type": "Effect Type:", + "color_strip.effect.type.hint": "Choose the procedural algorithm.", + "color_strip.effect.fire": "Fire", + "color_strip.effect.fire.desc": "Cellular automaton simulating rising flames with heat diffusion", + "color_strip.effect.meteor": "Meteor", + "color_strip.effect.meteor.desc": "Bright head travels along the strip with an exponential-decay tail", + "color_strip.effect.plasma": "Plasma", + "color_strip.effect.plasma.desc": "Overlapping sine waves mapped to a palette — classic demo-scene effect", + "color_strip.effect.noise": "Noise", + "color_strip.effect.noise.desc": "Scrolling fractal value noise mapped to a palette", + "color_strip.effect.aurora": "Aurora", + "color_strip.effect.aurora.desc": "Layered noise bands that drift and blend — northern lights style", + "color_strip.effect.rain": "Rain", + "color_strip.effect.rain.desc": "Raindrops fall down the strip with trailing tails", + "color_strip.effect.comet": "Comet", + "color_strip.effect.comet.desc": "Multiple comets with curved, pulsing tails", + "color_strip.effect.bouncing_ball": "Bouncing Ball", + "color_strip.effect.bouncing_ball.desc": "Physics-simulated balls bouncing with gravity", + "color_strip.effect.fireworks": "Fireworks", + "color_strip.effect.fireworks.desc": "Rockets launch and explode into colorful bursts", + "color_strip.effect.sparkle_rain": "Sparkle Rain", + "color_strip.effect.sparkle_rain.desc": "Twinkling star field with smooth fade-in/fade-out", + "color_strip.effect.lava_lamp": "Lava Lamp", + "color_strip.effect.lava_lamp.desc": "Slow-moving colored blobs that merge and separate", + "color_strip.effect.wave_interference": "Wave Interference", + "color_strip.effect.wave_interference.desc": "Two counter-propagating waves creating interference patterns", + "color_strip.effect.custom_palette": "Custom Palette:", + "color_strip.effect.custom_palette.hint": "JSON array of [position, R, G, B] stops, e.g. [[0,0,0,0],[0.5,255,0,0],[1,255,255,0]]", + "color_strip.effect.speed": "Speed:", + "color_strip.effect.speed.hint": "Speed multiplier for the effect animation (0.1 = very slow, 10.0 = very fast).", + "color_strip.effect.palette": "Palette:", + "color_strip.effect.palette.hint": "Color palette used to map effect values to RGB colors.", + "color_strip.effect.color": "Meteor Color:", + "color_strip.effect.color.hint": "Head color for the meteor effect.", + "color_strip.effect.intensity": "Intensity:", + "color_strip.effect.intensity.hint": "Effect intensity — controls spark rate (fire), tail decay (meteor), or brightness range (aurora).", + "color_strip.effect.scale": "Scale:", + "color_strip.effect.scale.hint": "Spatial scale — wave frequency (plasma), zoom level (noise), or band width (aurora).", + "color_strip.effect.mirror": "Mirror:", + "color_strip.effect.mirror.hint": "Bounce mode — the meteor reverses direction at strip ends instead of wrapping.", + "color_strip.palette.fire": "Fire", + "color_strip.palette.ocean": "Ocean", + "color_strip.palette.lava": "Lava", + "color_strip.palette.forest": "Forest", + "color_strip.palette.rainbow": "Rainbow", + "color_strip.palette.aurora": "Aurora", + "color_strip.palette.sunset": "Sunset", + "color_strip.palette.ice": "Ice", + "color_strip.palette.custom": "Custom", + "audio_source.title": "Audio Sources", + "audio_source.group.multichannel": "Multichannel", + "audio_source.group.mono": "Mono", + "audio_source.group.band_extract": "Band Extract", + "audio_source.add": "Add Audio Source", + "audio_source.add.multichannel": "Add Multichannel Source", + "audio_source.add.mono": "Add Mono Source", + "audio_source.add.band_extract": "Add Band Extract Source", + "audio_source.edit": "Edit Audio Source", + "audio_source.edit.multichannel": "Edit Multichannel Source", + "audio_source.edit.mono": "Edit Mono Source", + "audio_source.edit.band_extract": "Edit Band Extract Source", + "audio_source.name": "Name:", + "audio_source.name.placeholder": "System Audio", + "audio_source.name.hint": "A descriptive name for this audio source", + "audio_source.type": "Type:", + "audio_source.type.hint": "Multichannel captures all channels from a physical audio device. Mono extracts a single channel from a multichannel source.", + "audio_source.type.multichannel": "Multichannel", + "audio_source.type.mono": "Mono", + "audio_source.device": "Audio Device:", + "audio_source.device.hint": "Audio input source. Loopback devices capture system audio output; input devices capture microphone or line-in.", + "audio_source.refresh_devices": "Refresh devices", + "audio_source.parent": "Parent Source:", + "audio_source.parent.hint": "Multichannel source to extract a channel from", + "audio_source.channel": "Channel:", + "audio_source.channel.hint": "Which audio channel to extract from the multichannel source", + "audio_source.channel.mono": "Mono (L+R mix)", + "audio_source.channel.left": "Left", + "audio_source.channel.right": "Right", + "audio_source.description": "Description (optional):", + "audio_source.description.placeholder": "Describe this audio source...", + "audio_source.description.hint": "Optional notes about this audio source", + "audio_source.created": "Audio source created", + "audio_source.updated": "Audio source updated", + "audio_source.deleted": "Audio source deleted", + "audio_source.delete.confirm": "Are you sure you want to delete this audio source?", + "audio_source.error.name_required": "Please enter a name", + "audio_source.audio_template": "Audio Template:", + "audio_source.audio_template.hint": "Audio capture template that defines which engine and settings to use for this device", + "audio_source.band_parent": "Parent Audio Source:", + "audio_source.band_parent.hint": "Audio source to extract the frequency band from", + "audio_source.band": "Frequency Band:", + "audio_source.band.hint": "Select a frequency band preset or custom range", + "audio_source.band.bass": "Bass (20–250 Hz)", + "audio_source.band.mid": "Mid (250–4000 Hz)", + "audio_source.band.treble": "Treble (4000–20000 Hz)", + "audio_source.band.custom": "Custom Range", + "audio_source.freq_low": "Low Frequency (Hz):", + "audio_source.freq_high": "High Frequency (Hz):", + "audio_source.freq_range": "Frequency Range", + "audio_source.test": "Test", + "audio_source.test.title": "Test Audio Source", + "audio_source.test.rms": "RMS", + "audio_source.test.peak": "Peak", + "audio_source.test.beat": "Beat", + "audio_source.test.connecting": "Connecting...", + "audio_source.test.error": "Audio test failed", + "audio_template.test": "Test", + "audio_template.test.title": "Test Audio Template", + "audio_template.test.device": "Audio Device:", + "audio_template.test.device.hint": "Select which audio device to capture from during the test", + "audio_template.test.run": "Run", + "audio_template.title": "Audio Templates", + "audio_template.add": "Add Audio Template", + "audio_template.edit": "Edit Audio Template", + "audio_template.name": "Template Name:", + "audio_template.name.placeholder": "My Audio Template", + "audio_template.description.label": "Description (optional):", + "audio_template.description.placeholder": "Describe this template...", + "audio_template.engine": "Audio Engine:", + "audio_template.engine.hint": "Select the audio capture backend to use. WASAPI is Windows-only with loopback support. Sounddevice is cross-platform.", + "audio_template.engine.unavailable": "Unavailable", + "audio_template.engine.unavailable.hint": "This engine is not available on your system", + "audio_template.config": "Configuration", + "audio_template.config.show": "Show configuration", + "audio_template.created": "Audio template created", + "audio_template.updated": "Audio template updated", + "audio_template.deleted": "Audio template deleted", + "audio_template.delete.confirm": "Are you sure you want to delete this audio template?", + "audio_template.error.load": "Failed to load audio templates", + "audio_template.error.engines": "Failed to load audio engines", + "audio_template.error.required": "Please fill in all required fields", + "audio_template.error.delete": "Failed to delete audio template", + "streams.group.value": "Value Sources", + "streams.group.sync": "Sync Clocks", + "streams.group.gradients": "Gradients", + "gradient.group.title": "Gradients", + "gradient.add": "Add Gradient", + "gradient.edit": "Edit Gradient", + "gradient.builtin": "Built-in", + "gradient.stops_label": "stops", + "gradient.name": "Name:", + "gradient.name.hint": "A descriptive name for this gradient.", + "gradient.description": "Description:", + "gradient.description.hint": "Optional description for this gradient.", + "gradient.created": "Gradient created", + "gradient.updated": "Gradient updated", + "gradient.cloned": "Gradient cloned", + "gradient.deleted": "Gradient deleted", + "gradient.error.name_required": "Name is required", + "gradient.error.min_stops": "At least 2 color stops are required", + "gradient.error.delete_failed": "Failed to delete gradient", + "gradient.create_name": "New gradient name:", + "gradient.edit_name": "Rename gradient:", + "gradient.confirm_delete": "Delete gradient \"{name}\"?", + "section.empty.gradients": "No gradients yet", + "tree.group.picture": "Picture Source", + "tree.group.capture": "Screen Capture", + "tree.group.static": "Static", + "tree.group.processing": "Processed", + "tree.group.strip": "Color Strip", + "tree.group.audio": "Audio", + "tree.group.integrations": "Integrations", + "tree.group.utility": "Utility", + "tree.leaf.sources": "Sources", + "tree.leaf.engine_templates": "Engine Templates", + "tree.leaf.images": "Images", + "tree.leaf.video": "Video", + "tree.leaf.filter_templates": "Filter Templates", + "tree.leaf.processing_templates": "Processing Templates", + "tree.leaf.templates": "Templates", + "value_source.group.title": "Value Sources", + "value_source.select_type": "Select Value Source Type", + "value_source.add": "Add Value Source", + "value_source.edit": "Edit Value Source", + "value_source.name": "Name:", + "value_source.name.placeholder": "Brightness Pulse", + "value_source.name.hint": "A descriptive name for this value source", + "value_source.type": "Type:", + "value_source.type.hint": "Static outputs a constant value. Animated cycles through a waveform. Audio reacts to sound input. Adaptive types adjust brightness automatically based on time of day or scene content.", + "value_source.type.static": "Static", + "value_source.type.static.desc": "Constant output value", + "value_source.type.animated": "Animated", + "value_source.type.animated.desc": "Cycles through a waveform", + "value_source.type.audio": "Audio", + "value_source.type.audio.desc": "Reacts to sound input", + "value_source.type.adaptive_time": "Adaptive (Time)", + "value_source.type.adaptive_time.desc": "Adjusts by time of day", + "value_source.type.adaptive_scene": "Adaptive (Scene)", + "value_source.type.adaptive_scene.desc": "Adjusts by scene content", + "value_source.type.daylight": "Daylight Cycle", + "value_source.type.daylight.desc": "Brightness follows day/night cycle", + "value_source.daylight.speed": "Speed:", + "value_source.daylight.speed.hint": "Cycle speed multiplier. 1.0 = full day/night cycle in ~4 minutes. Higher values cycle faster.", + "value_source.daylight.use_real_time": "Use Real Time:", + "value_source.daylight.use_real_time.hint": "When enabled, brightness follows the actual time of day. Speed is ignored.", + "value_source.daylight.enable_real_time": "Follow wall clock", + "value_source.daylight.latitude": "Latitude:", + "value_source.daylight.latitude.hint": "Your geographic latitude (-90 to 90). Affects sunrise/sunset timing in real-time mode.", + "value_source.daylight.real_time": "Real-time", + "value_source.daylight.speed_label": "Speed", + "value_source.value": "Value:", + "value_source.value.hint": "Constant output value (0.0 = off, 1.0 = full brightness)", + "value_source.waveform": "Waveform:", + "value_source.waveform.hint": "Shape of the brightness animation cycle", + "value_source.waveform.sine": "Sine", + "value_source.waveform.triangle": "Triangle", + "value_source.waveform.square": "Square", + "value_source.waveform.sawtooth": "Sawtooth", + "value_source.speed": "Speed (cpm):", + "value_source.speed.hint": "Cycles per minute — how fast the waveform repeats (1 = very slow, 120 = very fast)", + "value_source.min_value": "Min Value:", + "value_source.min_value.hint": "Minimum output of the waveform cycle", + "value_source.max_value": "Max Value:", + "value_source.max_value.hint": "Maximum output of the waveform cycle", + "value_source.audio_source": "Audio Source:", + "value_source.audio_source.hint": "Audio source to read audio levels from (multichannel or mono)", + "value_source.mode": "Mode:", + "value_source.mode.hint": "RMS measures average volume. Peak tracks loudest moments. Beat triggers on rhythm.", + "value_source.mode.rms": "RMS (Volume)", + "value_source.mode.peak": "Peak", + "value_source.mode.beat": "Beat", + "value_source.mode.rms.desc": "Average volume level", + "value_source.mode.peak.desc": "Loudest moment tracking", + "value_source.mode.beat.desc": "Rhythm pulse detection", + "value_source.auto_gain": "Auto Gain:", + "value_source.auto_gain.hint": "Automatically normalize audio levels so output uses the full range, regardless of input volume", + "value_source.auto_gain.enable": "Enable auto-gain", + "value_source.sensitivity": "Sensitivity:", + "value_source.sensitivity.hint": "Gain multiplier for the audio signal (higher = more reactive)", + "value_source.scene_sensitivity.hint": "Gain multiplier for the luminance signal (higher = more reactive to brightness changes)", + "value_source.smoothing": "Smoothing:", + "value_source.smoothing.hint": "Temporal smoothing (0 = instant response, 1 = very smooth/slow)", + "value_source.audio_min_value": "Min Value:", + "value_source.audio_min_value.hint": "Output when audio is silent (e.g. 0.3 = 30% brightness floor)", + "value_source.audio_max_value": "Max Value:", + "value_source.audio_max_value.hint": "Output at maximum audio level", + "value_source.schedule": "Schedule:", + "value_source.schedule.hint": "Define at least 2 time points. Brightness interpolates linearly between them, wrapping at midnight.", + "value_source.schedule.add": "+ Add Point", + "value_source.schedule.points": "points", + "value_source.picture_source": "Picture Source:", + "value_source.picture_source.hint": "The picture source whose frames will be analyzed for average brightness.", + "value_source.scene_behavior": "Behavior:", + "value_source.scene_behavior.hint": "Complement: dark scene = high brightness (ideal for ambient backlight). Match: bright scene = high brightness.", + "value_source.scene_behavior.complement": "Complement (dark → bright)", + "value_source.scene_behavior.match": "Match (bright → bright)", + "value_source.adaptive_min_value": "Min Value:", + "value_source.adaptive_min_value.hint": "Minimum output brightness", + "value_source.adaptive_max_value": "Max Value:", + "value_source.adaptive_max_value.hint": "Maximum output brightness", + "value_source.error.schedule_min": "Schedule requires at least 2 time points", + "value_source.description": "Description (optional):", + "value_source.description.placeholder": "Describe this value source...", + "value_source.description.hint": "Optional notes about this value source", + "value_source.created": "Value source created", + "value_source.updated": "Value source updated", + "value_source.deleted": "Value source deleted", + "value_source.delete.confirm": "Are you sure you want to delete this value source?", + "value_source.error.name_required": "Please enter a name", + "value_source.test": "Test", + "value_source.test.title": "Test Value Source", + "value_source.test.connecting": "Connecting...", + "value_source.test.error": "Failed to connect", + "value_source.test.current": "Current", + "value_source.test.min": "Min", + "value_source.test.max": "Max", + "test.frames": "Frames", + "test.fps": "FPS", + "test.avg_capture": "Avg", + "targets.brightness_vs": "Brightness Source:", + "targets.brightness_vs.hint": "Optional value source that dynamically controls brightness each frame (overrides device brightness)", + "targets.brightness_vs.none": "None (device brightness)", + "targets.min_brightness_threshold": "Min Brightness Threshold:", + "targets.min_brightness_threshold.hint": "Effective output brightness (pixel brightness × device/source brightness) below this value turns LEDs off completely (0 = disabled)", + "targets.adaptive_fps": "Adaptive FPS:", + "targets.adaptive_fps.hint": "Automatically reduce send rate when the device becomes unresponsive, and gradually recover when it stabilizes. Recommended for WiFi devices with weak signal.", + "targets.protocol": "Protocol:", + "targets.protocol.hint": "DDP sends pixels via fast UDP (recommended for most setups). HTTP uses the JSON API — slower but reliable, limited to ~500 LEDs.", + "targets.protocol.ddp": "DDP (UDP)", + "targets.protocol.ddp.desc": "Fast raw UDP packets — recommended", + "targets.protocol.http": "HTTP", + "targets.protocol.http.desc": "JSON API — slower, ≤500 LEDs", + "targets.protocol.serial": "Serial", + "search.open": "Search (Ctrl+K)", + "search.placeholder": "Search entities... (Ctrl+K)", + "search.loading": "Loading...", + "search.no_results": "No results found", + "search.group.devices": "Devices", + "search.group.targets": "LED Targets", + "search.group.kc_targets": "Key Colors Targets", + "search.group.css": "Color Strip Sources", + "search.group.automations": "Automations", + "search.group.streams": "Picture Streams", + "search.group.capture_templates": "Capture Templates", + "search.group.pp_templates": "Post-Processing Templates", + "search.group.pattern_templates": "Pattern Templates", + "search.group.audio": "Audio Sources", + "search.group.value": "Value Sources", + "search.group.scenes": "Scene Presets", + "search.group.cspt": "Strip Processing Templates", + "search.group.sync_clocks": "Sync Clocks", + "search.group.actions": "Actions", + "search.action.start": "Start", + "search.action.stop": "Stop", + "search.action.activate": "Activate", + "search.action.enable": "Enable", + "search.action.disable": "Disable", + "settings.backup.label": "Backup Configuration", + "settings.backup.hint": "Download all configuration (devices, targets, streams, templates, automations) as a single JSON file.", + "settings.backup.button": "Download Backup", + "settings.backup.success": "Backup downloaded successfully", + "settings.backup.error": "Backup download failed", + "settings.restore.label": "Restore Configuration", + "settings.restore.hint": "Upload a previously downloaded backup file to replace all configuration. The server will restart automatically.", + "settings.restore.button": "Restore from Backup", + "settings.restore.confirm": "This will replace ALL configuration and restart the server. Are you sure?", + "settings.restore.success": "Configuration restored", + "settings.restore.error": "Restore failed", + "settings.restore.restarting": "Server is restarting...", + "settings.restore.restart_timeout": "Server did not respond. Please refresh the page manually.", + "settings.restart_server": "Restart Server", + "settings.restart_confirm": "Restart the server? Active targets will be stopped.", + "settings.restarting": "Restarting server...", + "settings.button.close": "Close", + "settings.log_level.label": "Log Level", + "settings.log_level.hint": "Change the server log verbosity at runtime. DEBUG shows the most detail; CRITICAL shows only fatal errors.", + "settings.log_level.save": "Apply", + "settings.log_level.saved": "Log level changed", + "settings.log_level.save_error": "Failed to change log level", + "settings.log_level.desc.debug": "Verbose developer output", + "settings.log_level.desc.info": "Normal operation messages", + "settings.log_level.desc.warning": "Potential problems", + "settings.log_level.desc.error": "Failures only", + "settings.log_level.desc.critical": "Fatal errors only", + "settings.auto_backup.label": "Auto-Backup", + "settings.auto_backup.hint": "Automatically create periodic backups of all configuration. Old backups are pruned when the maximum count is reached.", + "settings.auto_backup.enable": "Enable auto-backup", + "settings.auto_backup.interval_label": "Interval", + "settings.auto_backup.max_label": "Max backups", + "settings.auto_backup.save": "Save Settings", + "settings.auto_backup.saved": "Auto-backup settings saved", + "settings.auto_backup.save_error": "Failed to save auto-backup settings", + "settings.auto_backup.backup_now": "Backup Now", + "settings.auto_backup.backup_created": "Backup created", + "settings.auto_backup.backup_error": "Backup failed", + "settings.auto_backup.last_backup": "Last backup", + "settings.auto_backup.never": "Never", + "settings.saved_backups.label": "Saved Backups", + "settings.saved_backups.hint": "Auto-backup files stored on the server. Download to save locally, or delete to free space.", + "settings.saved_backups.empty": "No saved backups", + "settings.saved_backups.restore": "Restore", + "settings.saved_backups.download": "Download", + "settings.saved_backups.delete": "Delete", + "settings.saved_backups.delete_confirm": "Delete this backup file?", + "settings.saved_backups.delete_error": "Failed to delete backup", + "settings.saved_backups.type.auto": "auto", + "settings.saved_backups.type.manual": "manual", + "settings.mqtt.label": "MQTT", + "settings.mqtt.hint": "Configure MQTT broker connection for automation conditions and triggers.", + "settings.mqtt.enabled": "Enable MQTT", + "settings.mqtt.host_label": "Broker Host", + "settings.mqtt.port_label": "Port", + "settings.mqtt.username_label": "Username", + "settings.mqtt.password_label": "Password", + "settings.mqtt.password_set_hint": "Password is set — leave blank to keep", + "settings.mqtt.client_id_label": "Client ID", + "settings.mqtt.base_topic_label": "Base Topic", + "settings.mqtt.save": "Save MQTT Settings", + "settings.mqtt.saved": "MQTT settings saved", + "settings.mqtt.save_error": "Failed to save MQTT settings", + "settings.mqtt.error_host_required": "Broker host is required", + "settings.logs.label": "Server Logs", + "settings.logs.hint": "Stream live server log output. Use the filter to show only relevant log levels.", + "settings.logs.connect": "Connect", + "settings.logs.disconnect": "Disconnect", + "settings.logs.clear": "Clear", + "settings.logs.error": "Log viewer connection failed", + "settings.logs.filter.all": "All levels", + "settings.logs.filter.info": "Info+", + "settings.logs.filter.warning": "Warning+", + "settings.logs.filter.error": "Error only", + "settings.logs.filter.all_desc": "Show all log messages", + "settings.logs.filter.info_desc": "Info, warning, and errors", + "settings.logs.filter.warning_desc": "Warnings and errors only", + "settings.logs.filter.error_desc": "Errors only", + "device.error.power_off_failed": "Failed to turn off device", + "device.error.remove_failed": "Failed to remove device", + "device.error.settings_load_failed": "Failed to load device settings", + "device.error.brightness": "Failed to update brightness", + "device.error.required": "Please fill in all fields correctly", + "device.error.update": "Failed to update device", + "device.error.save": "Failed to save settings", + "device.error.clone_failed": "Failed to clone device", + "device_discovery.error.fill_all_fields": "Please fill in all fields", + "device_discovery.added": "Device added successfully", + "device_discovery.error.add_failed": "Failed to add device", + "calibration.error.load_failed": "Failed to load calibration", + "calibration.error.css_load_failed": "Failed to load color strip source", + "calibration.error.test_toggle_failed": "Failed to toggle test edge", + "calibration.error.save_failed": "Failed to save calibration", + "calibration.error.led_count_mismatch": "Total LEDs must equal the device LED count", + "calibration.error.led_count_exceeded": "Calibrated LEDs exceed the total LED count", + "calibration.mode.simple": "Simple", + "calibration.mode.advanced": "Advanced", + "calibration.switch_to_advanced": "Switch to Advanced", + "calibration.advanced.title": "Advanced Calibration", + "calibration.advanced.switch_to_simple": "Switch to Simple", + "calibration.advanced.lines_title": "Lines", + "calibration.advanced.canvas_hint": "Drag monitors to reposition. Click edges to select lines. Scroll to zoom, drag empty space to pan.", + "calibration.advanced.reset_view": "Reset view", + "calibration.advanced.line_properties": "Line Properties", + "calibration.advanced.picture_source": "Source:", + "calibration.advanced.picture_source.hint": "The picture source (monitor) this line samples from", + "calibration.advanced.edge": "Edge:", + "calibration.advanced.edge.hint": "Which screen edge to sample pixels from", + "calibration.advanced.led_count": "LEDs:", + "calibration.advanced.led_count.hint": "Number of LEDs mapped to this line", + "calibration.advanced.span_start": "Span Start:", + "calibration.advanced.span_start.hint": "Where sampling begins along the edge (0 = start, 1 = end). Use to cover only part of an edge.", + "calibration.advanced.span_end": "Span End:", + "calibration.advanced.span_end.hint": "Where sampling ends along the edge (0 = start, 1 = end). Together with Span Start, defines the active portion.", + "calibration.advanced.border_width": "Depth (px):", + "calibration.advanced.border_width.hint": "How many pixels deep from the edge to sample. Larger values capture more of the screen interior.", + "calibration.advanced.reverse": "Reverse", + "calibration.advanced.no_lines_warning": "Add at least one line", + "dashboard.error.automation_toggle_failed": "Failed to toggle automation", + "dashboard.error.start_failed": "Failed to start processing", + "dashboard.error.stop_failed": "Failed to stop processing", + "dashboard.error.stop_all": "Failed to stop all targets", + "target.error.editor_open_failed": "Failed to open target editor", + "target.error.start_failed": "Failed to start target", + "target.error.stop_failed": "Failed to stop target", + "target.error.clone_failed": "Failed to clone target", + "target.error.delete_failed": "Failed to delete target", + "targets.stop_all.button": "Stop All", + "targets.stop_all.none_running": "No targets are currently running", + "targets.stop_all.stopped": "Stopped {count} target(s)", + "targets.stop_all.error": "Failed to stop targets", + "audio_source.error.load": "Failed to load audio source", + "audio_template.error.clone_failed": "Failed to clone audio template", + "value_source.error.load": "Failed to load value source", + "color_strip.error.editor_open_failed": "Failed to open color strip editor", + "color_strip.error.clone_failed": "Failed to clone color strip source", + "color_strip.error.delete_failed": "Failed to delete color strip source", + "pattern.error.editor_open_failed": "Failed to open pattern template editor", + "pattern.error.clone_failed": "Failed to clone pattern template", + "pattern.error.delete_failed": "Failed to delete pattern template", + "pattern.error.capture_bg_failed": "Failed to capture background", + "stream.error.clone_picture_failed": "Failed to clone picture source", + "stream.error.clone_capture_failed": "Failed to clone capture template", + "stream.error.clone_pp_failed": "Failed to clone postprocessing template", + "theme.switched.dark": "Switched to dark theme", + "theme.switched.light": "Switched to light theme", + "accent.color.updated": "Accent color updated", + "search.footer": "↑↓ navigate · Enter select · Esc close", + "sync_clock.group.title": "Sync Clocks", + "sync_clock.add": "Add Sync Clock", + "sync_clock.edit": "Edit Sync Clock", + "sync_clock.name": "Name:", + "sync_clock.name.placeholder": "Main Animation Clock", + "sync_clock.name.hint": "A descriptive name for this synchronization clock", + "sync_clock.speed": "Speed:", + "sync_clock.speed.hint": "Animation speed multiplier for all linked sources. 1.0 = normal, 2.0 = double speed, 0.5 = half speed.", + "sync_clock.description": "Description (optional):", + "sync_clock.description.placeholder": "Optional description", + "sync_clock.description.hint": "Optional notes about this clock's purpose", + "sync_clock.status.running": "Running", + "sync_clock.status.paused": "Paused", + "sync_clock.action.pause": "Pause", + "sync_clock.action.resume": "Resume", + "sync_clock.action.reset": "Reset", + "sync_clock.error.name_required": "Clock name is required", + "sync_clock.error.load": "Failed to load sync clock", + "sync_clock.created": "Sync clock created", + "sync_clock.updated": "Sync clock updated", + "sync_clock.deleted": "Sync clock deleted", + "sync_clock.paused": "Clock paused", + "sync_clock.resumed": "Clock resumed", + "sync_clock.reset_done": "Clock reset to zero", + "sync_clock.delete.confirm": "Delete this sync clock? Linked sources will lose synchronization and run at default speed.", + "sync_clock.elapsed": "Elapsed time", + "weather_source.group.title": "Weather Sources", + "weather_source.add": "Add Weather Source", + "weather_source.edit": "Edit Weather Source", + "weather_source.name": "Name:", + "weather_source.name.placeholder": "My Weather", + "weather_source.name.hint": "A descriptive name for this weather data source", + "weather_source.provider": "Provider:", + "weather_source.provider.hint": "Weather data provider. Open-Meteo is free and requires no API key.", + "weather_source.provider.open_meteo.desc": "Free, no API key required", + "weather_source.location": "Location:", + "weather_source.location.hint": "Your geographic coordinates. Use the auto-detect button or enter manually.", + "weather_source.latitude": "Lat:", + "weather_source.longitude": "Lon:", + "weather_source.use_my_location": "Use my location", + "weather_source.update_interval": "Update Interval:", + "weather_source.update_interval.hint": "How often to fetch weather data. Lower values give more responsive changes.", + "weather_source.description": "Description (optional):", + "weather_source.description.placeholder": "Optional description", + "weather_source.test": "Test", + "weather_source.error.name_required": "Weather source name is required", + "weather_source.error.load": "Failed to load weather source", + "weather_source.created": "Weather source created", + "weather_source.updated": "Weather source updated", + "weather_source.deleted": "Weather source deleted", + "weather_source.delete.confirm": "Delete this weather source? Linked color strip sources will lose weather data.", + "weather_source.geo.success": "Location detected", + "weather_source.geo.error": "Geolocation failed", + "weather_source.geo.not_supported": "Geolocation is not supported by your browser", + "streams.group.weather": "Weather", + "streams.group.home_assistant": "Home Assistant", + "ha_source.group.title": "Home Assistant Sources", + "ha_source.add": "Add Home Assistant Source", + "ha_source.edit": "Edit Home Assistant Source", + "ha_source.name": "Name:", + "ha_source.name.placeholder": "My Home Assistant", + "ha_source.name.hint": "A descriptive name for this Home Assistant connection", + "ha_source.host": "Host:", + "ha_source.host.hint": "Home Assistant host and port, e.g. 192.168.1.100:8123", + "ha_source.token": "Access Token:", + "ha_source.token.hint": "Long-Lived Access Token from HA (Profile > Security > Long-Lived Access Tokens)", + "ha_source.token.edit_hint": "Leave blank to keep the current token", + "ha_source.use_ssl": "Use SSL (wss://)", + "ha_source.entity_filters": "Entity Filters (optional):", + "ha_source.entity_filters.hint": "Comma-separated glob patterns, e.g. sensor.*, binary_sensor.front_door. Leave empty for all.", + "ha_source.description": "Description (optional):", + "ha_source.test": "Test Connection", + "ha_source.test.success": "Connected", + "ha_source.test.failed": "Connection failed", + "ha_source.connected": "Connected", + "ha_source.disconnected": "Disconnected", + "ha_source.error.name_required": "Name is required", + "ha_source.error.host_required": "Host is required", + "ha_source.error.token_required": "Access token is required", + "ha_source.error.load": "Failed to load Home Assistant source", + "ha_source.created": "Home Assistant source created", + "ha_source.updated": "Home Assistant source updated", + "ha_source.deleted": "Home Assistant source deleted", + "ha_source.delete.confirm": "Delete this Home Assistant connection?", + "section.empty.ha_sources": "No Home Assistant sources yet. Click + to add one.", + "ha_light.section.title": "Home Assistant", + "ha_light.section.targets": "Light Targets", + "ha_light.add": "Add HA Light Target", + "ha_light.edit": "Edit HA Light Target", + "ha_light.name": "Name:", + "ha_light.name.placeholder": "Living Room Lights", + "ha_light.ha_source": "HA Connection:", + "ha_light.css_source": "Color Strip Source:", + "ha_light.update_rate": "Update Rate:", + "ha_light.update_rate.hint": "How often to send color updates to HA lights (0.5-5.0 Hz). Lower values are safer for HA performance.", + "ha_light.transition": "Transition:", + "ha_light.transition.hint": "Smooth fade duration between colors (HA transition parameter).", + "ha_light.mappings": "Light Mappings:", + "ha_light.mappings.hint": "Map LED ranges to HA light entities. Each mapping averages the LED segment to a single color.", + "ha_light.mappings.add": "Add Mapping", + "ha_light.mapping.entity_id": "Entity ID:", + "ha_light.mapping.led_start": "LED Start:", + "ha_light.mapping.led_end": "LED End (-1=last):", + "ha_light.mapping.brightness": "Brightness Scale:", + "ha_light.description": "Description (optional):", + "ha_light.error.name_required": "Name is required", + "ha_light.error.ha_source_required": "HA connection is required", + "ha_light.created": "HA light target created", + "ha_light.updated": "HA light target updated", + "ha_light.mapping.select_entity": "Select a light entity...", + "ha_light.mapping.search_entity": "Search light entities...", + "section.empty.ha_light_targets": "No HA light targets yet. Click + to add one.", + "automations.condition.home_assistant": "Home Assistant", + "automations.condition.home_assistant.desc": "HA entity state", + "automations.condition.home_assistant.ha_source": "HA Source:", + "automations.condition.home_assistant.entity_id": "Entity ID:", + "automations.condition.home_assistant.state": "State:", + "automations.condition.home_assistant.match_mode": "Match Mode:", + "automations.condition.home_assistant.hint": "Activate when a Home Assistant entity matches the specified state", + "color_strip.clock": "Sync Clock:", + "color_strip.clock.hint": "Link to a sync clock to synchronize animation timing across sources. Speed is controlled on the clock.", + "graph.title": "Graph", + "graph.fit_all": "Fit all nodes", + "graph.zoom_in": "Zoom in", + "graph.zoom_out": "Zoom out", + "graph.search": "Search nodes", + "graph.search_placeholder": "Search entities...", + "graph.legend": "Legend", + "graph.minimap": "Minimap", + "graph.relayout": "Re-layout", + "graph.empty": "No entities yet", + "graph.empty.hint": "Create devices, sources, and targets to see them here.", + "graph.disconnect": "Disconnect", + "graph.connection_updated": "Connection updated", + "graph.connection_failed": "Failed to update connection", + "graph.connection_removed": "Connection removed", + "graph.disconnect_failed": "Failed to disconnect", + "graph.relayout_confirm": "Reset all manual node positions and re-layout the graph?", + "graph.fullscreen": "Toggle fullscreen", + "graph.add_entity": "Add entity", + "graph.color_picker": "Node color", + "graph.filter": "Filter nodes", + "graph.filter_placeholder": "Filter: name, type:x, tag:x", + "graph.filter_clear": "Clear filter", + "graph.filter_running": "Running", + "graph.filter_stopped": "Stopped", + "graph.filter_types": "Types", + "graph.filter_group.capture": "Capture", + "graph.filter_group.strip": "Color Strip", + "graph.filter_group.audio": "Audio", + "graph.filter_group.targets": "Targets", + "graph.filter_group.other": "Other", + "graph.bulk_delete_confirm": "Delete {count} selected entities?", + "graph.nothing_to_undo": "Nothing to undo", + "graph.nothing_to_redo": "Nothing to redo", + "graph.help_title": "Keyboard Shortcuts", + "graph.help.search": "Search", + "graph.help.filter": "Filter", + "graph.help.add": "Add entity", + "graph.help.shortcuts": "Shortcuts", + "graph.help.delete": "Delete / Detach", + "graph.help.select_all": "Select all", + "graph.help.undo": "Undo", + "graph.help.redo": "Redo", + "graph.help.fullscreen": "Fullscreen", + "graph.help.deselect": "Deselect", + "graph.help.navigate": "Navigate nodes", + "graph.help.click": "Click", + "graph.help.click_desc": "Select node", + "graph.help.dblclick": "Double-click", + "graph.help.dblclick_desc": "Zoom to node", + "graph.help.shift_click": "Shift+Click", + "graph.help.shift_click_desc": "Multi-select", + "graph.help.shift_drag": "Shift+Drag", + "graph.help.shift_drag_desc": "Rubber-band select", + "graph.help.drag_node": "Drag node", + "graph.help.drag_node_desc": "Reposition", + "graph.help.drag_port": "Drag port", + "graph.help.drag_port_desc": "Connect entities", + "graph.help.right_click": "Right-click edge", + "graph.help.right_click_desc": "Detach connection", + "graph.tooltip.fps": "FPS", + "graph.tooltip.errors": "Errors", + "graph.tooltip.uptime": "Uptime", + "automation.enabled": "Automation enabled", + "automation.disabled": "Automation disabled", + "scene_preset.activated": "Preset activated", + "scene_preset.used_by": "Used by %d automation(s)", + "settings.api_keys.label": "API Keys", + "settings.api_keys.hint": "API keys are defined in the server config file (config.yaml). Edit the file and restart the server to apply changes.", + "settings.api_keys.empty": "No API keys configured", + "settings.api_keys.load_error": "Failed to load API keys", + "settings.partial.label": "Partial Export / Import", + "settings.partial.hint": "Export or import a single entity type. Import replaces or merges existing data and restarts the server.", + "settings.partial.store.devices": "Devices", + "settings.partial.store.output_targets": "LED Targets", + "settings.partial.store.color_strip_sources": "Color Strips", + "settings.partial.store.picture_sources": "Picture Sources", + "settings.partial.store.audio_sources": "Audio Sources", + "settings.partial.store.audio_templates": "Audio Templates", + "settings.partial.store.capture_templates": "Capture Templates", + "settings.partial.store.postprocessing_templates": "Post-processing Templates", + "settings.partial.store.color_strip_processing_templates": "CSS Processing Templates", + "settings.partial.store.pattern_templates": "Pattern Templates", + "settings.partial.store.value_sources": "Value Sources", + "settings.partial.store.sync_clocks": "Sync Clocks", + "settings.partial.store.automations": "Automations", + "settings.partial.store.scene_presets": "Scene Presets", + "settings.partial.export_button": "Export", + "settings.partial.import_button": "Import from File", + "settings.partial.merge_label": "Merge (add/overwrite, keep existing)", + "settings.partial.export_success": "Exported successfully", + "settings.partial.export_error": "Export failed", + "settings.partial.import_success": "Imported successfully", + "settings.partial.import_error": "Import failed", + "settings.partial.import_confirm_replace": "This will REPLACE all {store} data and restart the server. Continue?", + "settings.partial.import_confirm_merge": "This will MERGE into existing {store} data and restart the server. Continue?", + "section.empty.devices": "No devices yet. Click + to add one.", + "section.empty.targets": "No LED targets yet. Click + to add one.", + "section.empty.kc_targets": "No key color targets yet. Click + to add one.", + "section.empty.pattern_templates": "No pattern templates yet. Click + to add one.", + "section.empty.picture_sources": "No sources yet. Click + to add one.", + "section.empty.capture_templates": "No capture templates yet. Click + to add one.", + "section.empty.pp_templates": "No post-processing templates yet. Click + to add one.", + "section.empty.audio_sources": "No audio sources yet. Click + to add one.", + "section.empty.audio_templates": "No audio templates yet. Click + to add one.", + "section.empty.color_strips": "No color strips yet. Click + to add one.", + "section.empty.value_sources": "No value sources yet. Click + to add one.", + "section.empty.sync_clocks": "No sync clocks yet. Click + to add one.", + "section.empty.weather_sources": "No weather sources yet. Click + to add one.", + "section.empty.cspt": "No CSS processing templates yet. Click + to add one.", + "section.empty.automations": "No automations yet. Click + to add one.", + "section.empty.scenes": "No scene presets yet. Click + to add one.", + "bulk.select": "Select", + "bulk.cancel": "Cancel", + "bulk.selected_count.one": "{count} selected", + "bulk.selected_count.other": "{count} selected", + "bulk.select_all": "Select all", + "bulk.deselect_all": "Deselect all", + "bulk.delete": "Delete", + "bulk.start": "Start", + "bulk.stop": "Stop", + "bulk.enable": "Enable", + "bulk.disable": "Disable", + "bulk.confirm_delete.one": "Delete {count} item?", + "bulk.confirm_delete.other": "Delete {count} items?", + "appearance.style.label": "Style Presets", + "appearance.style.hint": "Choose a visual theme — font pairing and color palette applied together.", + "appearance.preset.default": "Default", + "appearance.preset.midnight": "Midnight", + "appearance.preset.ember": "Ember", + "appearance.preset.arctic": "Arctic", + "appearance.preset.terminal": "Terminal", + "appearance.preset.neon": "Neon", + "appearance.preset.sakura": "Sakura", + "appearance.preset.ocean": "Ocean", + "appearance.preset.copper": "Copper", + "appearance.preset.vapor": "Vapor", + "appearance.preset.monolith": "Monolith", + "appearance.preset.applied": "Style preset applied", + "appearance.bg.label": "Background Effects", + "appearance.bg.hint": "Add an ambient background layer behind the interface.", + "appearance.bg.none": "None", + "appearance.bg.noise": "Noise Field", + "appearance.bg.aurora": "Aurora", + "appearance.bg.plasma": "Plasma", + "appearance.bg.rain": "Digital Rain", + "appearance.bg.stars": "Starfield", + "appearance.bg.warp": "Warp Tunnel", + "appearance.bg.grid": "Dot Grid", + "appearance.bg.mesh": "Gradient Mesh", + "appearance.bg.scanlines": "Scanlines", + "appearance.bg.applied": "Background effect applied", + "settings.tab.updates": "Updates", + "settings.tab.about": "About", + "update.status_label": "Update Status", + "update.current_version": "Current version:", + "update.badge_tooltip": "New version available — click for details", + "update.available": "Version {version} is available", + "update.up_to_date": "You are running the latest version", + "update.prerelease": "pre-release", + "update.view_release": "View Release", + "update.dismiss": "Dismiss", + "update.check_now": "Check for Updates", + "update.check_error": "Update check failed", + "update.last_check": "Last check", + "update.never": "never", + "update.release_notes": "Release Notes", + "update.view_release_notes": "View Release Notes", + "update.auto_check_label": "Auto-Check Settings", + "update.auto_check_hint": "Periodically check for new releases in the background.", + "update.enable": "Enable auto-check", + "update.interval_label": "Check interval", + "update.channel_label": "Channel", + "update.channel.stable": "Stable", + "update.channel.stable_desc": "Stable releases only", + "update.channel.prerelease": "Pre-release", + "update.channel.prerelease_desc": "Include alpha, beta, and RC builds", + "update.save_settings": "Save Settings", + "update.settings_saved": "Update settings saved", + "update.settings_save_error": "Failed to save update settings", + "update.apply_now": "Update Now", + "update.apply_confirm": "Download and install version {version}? The server will restart automatically.", + "update.apply_error": "Update failed", + "update.applying": "Applying update…", + "update.downloading": "Downloading…", + "update.install_type_label": "Install type:", + "update.install_type.installer": "Windows installer", + "update.install_type.portable": "Portable", + "update.install_type.docker": "Docker", + "update.install_type.dev": "Development", + "color_strip.notification.search_apps": "Search notification apps…", + "asset.group.title": "Assets", + "asset.upload": "Upload Asset", + "asset.edit": "Edit Asset", + "asset.name": "Name:", + "asset.name.hint": "Display name for this asset.", + "asset.description": "Description:", + "asset.description.hint": "Optional description for this asset.", + "asset.file": "File:", + "asset.file.hint": "Select a file to upload (sound, image, video, or other).", + "asset.drop_or_browse": "Drop file here or click to browse", + "asset.uploaded": "Asset uploaded", + "asset.updated": "Asset updated", + "asset.deleted": "Asset deleted", + "asset.confirm_delete": "Delete this asset?", + "asset.error.name_required": "Name is required", + "asset.error.no_file": "Please select a file to upload", + "asset.error.delete_failed": "Failed to delete asset", + "asset.error.play_failed": "Failed to play sound", + "asset.error.download_failed": "Failed to download asset", + "asset.play": "Play", + "asset.download": "Download", + "asset.prebuilt": "Prebuilt", + "asset.prebuilt_restored": "{count} prebuilt asset(s) restored", + "asset.prebuilt_none_to_restore": "All prebuilt assets are already available", + "asset.restore_prebuilt": "Restore Prebuilt Sounds", + "asset.type.sound": "Sound", + "asset.type.image": "Image", + "asset.type.video": "Video", + "asset.type.other": "Other", + "streams.group.assets": "Assets", + "section.empty.assets": "No assets yet. Click + to upload one.", + "donation.message": "LedGrab is free & open-source. If it's useful to you, consider supporting development.", + "donation.support": "Support the project", + "donation.view_source": "View source code", + "donation.later": "Remind me later", + "donation.dismiss": "Don't show again", + "donation.about_title": "About LedGrab", + "donation.about_opensource": "LedGrab is open-source software, free to use and modify.", + "donation.about_donate": "Support development", + "donation.about_license": "MIT License" +} diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 478d077..4b2fbe9 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -1,1956 +1,1913 @@ { - "app.title": "LED Grab", - "app.version": "Версия:", - "app.api_docs": "Документация API", - "app.connection_lost": "Сервер недоступен", - "app.connection_retrying": "Попытка переподключения…", - "app.server_restarting": "Сервер перезапускается…", - "app.server_restarting_sub": "Пожалуйста, подождите, сервер скоро вернётся.", - "demo.badge": "ДЕМО", - "demo.banner": "Вы в демо-режиме — все устройства и данные виртуальные. Реальное оборудование не используется.", - "theme.toggle": "Переключить тему", - "bg.anim.toggle": "Анимированный фон", - "accent.title": "Цвет акцента", - "accent.custom": "Свой", - "accent.reset": "Сброс", - "locale.change": "Изменить язык", - "auth.login": "Войти", - "auth.logout": "Выйти", - "auth.authenticated": "● Авторизован", - "auth.title": "Вход в LED Grab", - "auth.message": "Пожалуйста, введите ваш API ключ для аутентификации и доступа к LED Grab.", - "auth.label": "API Ключ:", - "auth.placeholder": "Введите ваш API ключ...", - "auth.hint": "Ваш API ключ будет безопасно сохранен в локальном хранилище браузера.", - "auth.button.cancel": "Отмена", - "auth.button.login": "Войти", - "auth.error.required": "Пожалуйста, введите API ключ", - "auth.success": "Вход выполнен успешно!", - "auth.logout.confirm": "Вы уверены, что хотите выйти?", - "auth.logout.success": "Выход выполнен успешно", - "auth.please_login": "Пожалуйста, войдите для просмотра", - "auth.session_expired": "Ваша сессия истекла или API ключ недействителен. Пожалуйста, войдите снова.", - "auth.toggle_password": "Показать/скрыть пароль", - "auth.prompt_enter": "Enter your API key:", - "auth.prompt_update": "Current API key is set. Enter new key to update or leave blank to remove:", - "api_key.login": "Войти", - "displays.title": "Доступные Дисплеи", - "displays.layout": "Дисплеи", - "displays.information": "Информация о Дисплеях", - "displays.legend.primary": "Основной Дисплей", - "displays.legend.secondary": "Вторичный Дисплей", - "displays.badge.primary": "Основной", - "displays.badge.secondary": "Вторичный", - "displays.resolution": "Разрешение:", - "displays.refresh_rate": "Частота Обновления:", - "displays.position": "Позиция:", - "displays.index": "Индекс Дисплея:", - "displays.loading": "Загрузка дисплеев...", - "displays.none": "Нет доступных дисплеев", - "displays.failed": "Не удалось загрузить дисплеи", - "displays.picker.title": "Выберите Дисплей", - "displays.picker.title.device": "Выберите Устройство", - "displays.picker.select": "Выберите дисплей...", - "displays.picker.click_to_select": "Нажмите, чтобы выбрать этот дисплей", - "displays.picker.adb_connect": "Подключить ADB устройство", - "displays.picker.adb_connect.placeholder": "IP адрес (напр. 192.168.2.201)", - "displays.picker.adb_connect.button": "Подключить", - "displays.picker.adb_connect.success": "Устройство подключено", - "displays.picker.adb_connect.error": "Не удалось подключить устройство", - "displays.picker.adb_disconnect": "Отключить", - "displays.picker.no_android": "Android устройства не найдены. Подключите по USB или введите IP выше.", - "templates.title": "Шаблоны Движков", - "templates.description": "Шаблоны захвата определяют, как захватывается экран. Каждый шаблон использует определённый движок захвата (MSS, DXcam, WGC) с настраиваемыми параметрами. Назначайте шаблоны устройствам для оптимальной производительности.", - "templates.loading": "Загрузка шаблонов...", - "templates.empty": "Шаблоны захвата не настроены", - "templates.add": "Добавить Шаблон Движка", - "templates.edit": "Редактировать Шаблон Движка", - "templates.name": "Имя Шаблона:", - "templates.name.placeholder": "Мой Пользовательский Шаблон", - "templates.description.label": "Описание (необязательно):", - "templates.description.placeholder": "Опишите этот шаблон...", - "templates.engine": "Движок Захвата:", - "templates.engine.hint": "Выберите технологию захвата экрана", - "templates.engine.select": "Выберите движок...", - "templates.engine.unavailable": "Недоступен", - "templates.engine.unavailable.hint": "Этот движок недоступен в вашей системе", - "templates.engine.mss.desc": "Кроссплатформенный, чистый Python", - "templates.engine.dxcam.desc": "DirectX, низкая задержка", - "templates.engine.bettercam.desc": "DirectX, высокая производительность", - "templates.engine.camera.desc": "Захват USB/IP камеры", - "templates.engine.scrcpy.desc": "Зеркалирование экрана Android", - "templates.engine.wgc.desc": "Windows Graphics Capture", - "templates.config": "Конфигурация", - "templates.config.show": "Показать конфигурацию", - "templates.config.none": "Нет дополнительных настроек", - "templates.config.default": "По умолчанию", - "templates.config.camera_backend.auto": "Автовыбор лучшего бэкенда", - "templates.config.camera_backend.dshow": "Windows DirectShow", - "templates.config.camera_backend.msmf": "Windows Media Foundation", - "templates.config.camera_backend.v4l2": "Linux Video4Linux2", - "templates.created": "Шаблон успешно создан", - "templates.updated": "Шаблон успешно обновлён", - "templates.deleted": "Шаблон успешно удалён", - "templates.delete.confirm": "Вы уверены, что хотите удалить этот шаблон?", - "templates.error.load": "Не удалось загрузить шаблоны", - "templates.error.engines": "Не удалось загрузить движки", - "templates.error.required": "Пожалуйста, заполните все обязательные поля", - "templates.error.delete": "Не удалось удалить шаблон", - "templates.test.title": "Тест Захвата", - "templates.test.description": "Протестируйте этот шаблон перед сохранением, чтобы увидеть предпросмотр захвата и метрики производительности.", - "templates.test.display": "Дисплей:", - "templates.test.display.select": "Выберите дисплей...", - "templates.test.duration": "Длительность Захвата (с):", - "templates.test.border_width": "Ширина Границы (px):", - "templates.test.run": "Запустить", - "templates.test.running": "Выполняется тест...", - "templates.test.results.preview": "Полный Предпросмотр Захвата", - "templates.test.results.borders": "Извлечение Границ", - "templates.test.results.top": "Сверху", - "templates.test.results.right": "Справа", - "templates.test.results.bottom": "Снизу", - "templates.test.results.left": "Слева", - "templates.test.results.performance": "Производительность", - "templates.test.results.capture_time": "Захват", - "templates.test.results.extraction_time": "Извлечение", - "templates.test.results.total_time": "Всего", - "templates.test.results.max_fps": "Макс. FPS", - "templates.test.results.duration": "Длительность", - "templates.test.results.frame_count": "Кадры", - "templates.test.results.actual_fps": "Факт. FPS", - "templates.test.results.avg_capture_time": "Средн. Захват", - "templates.test.results.resolution": "Разрешение:", - "templates.test.error.no_engine": "Пожалуйста, выберите движок захвата", - "templates.test.error.no_display": "Пожалуйста, выберите дисплей", - "templates.test.error.failed": "Тест не удался", - "devices.title": "Устройства", - "device.select_type": "Выберите тип устройства", - "devices.add": "Добавить Новое Устройство", - "devices.loading": "Загрузка устройств...", - "devices.none": "Устройства не настроены", - "devices.failed": "Не удалось загрузить устройства", - "devices.wled_config": "Конфигурация WLED:", - "devices.wled_note": "Настройте ваше WLED устройство (эффекты, сегменты, порядок цветов, ограничения питания и т.д.) используя", - "devices.wled_link": "официальное приложение WLED", - "devices.wled_note_or": "или встроенный", - "devices.wled_webui_link": "веб-интерфейс WLED", - "devices.wled_note_webui": "(откройте IP устройства в браузере).", - "devices.wled_note2": "Этот контроллер отправляет данные о цвете пикселей и управляет яркостью для каждого устройства.", - "device.scan": "Автопоиск", - "device.scan.empty": "Устройства не найдены", - "device.scan.error": "Ошибка сканирования сети", - "device.scan.already_added": "Уже добавлено", - "device.scan.selected": "Устройство выбрано", - "device.type": "Тип устройства:", - "device.type.hint": "Выберите тип LED контроллера", - "device.type.wled": "WLED", - "device.type.wled.desc": "WiFi LED контроллер по HTTP/UDP", - "device.type.adalight": "Adalight", - "device.type.adalight.desc": "Серийный протокол для Arduino", - "device.type.ambiled": "AmbiLED", - "device.type.ambiled.desc": "Серийный протокол AmbiLED", - "device.type.mqtt": "MQTT", - "device.type.mqtt.desc": "Отправка LED данных через MQTT брокер", - "device.type.ws": "WebSocket", - "device.type.ws.desc": "Стриминг LED данных через WebSocket", - "device.type.openrgb": "OpenRGB", - "device.type.openrgb.desc": "Управление RGB через OpenRGB", - "device.type.dmx": "DMX", - "device.type.dmx.desc": "Art-Net / sACN (E1.31) сценическое освещение", - "device.type.mock": "Mock", - "device.type.mock.desc": "Виртуальное устройство для тестов", - "device.type.espnow": "ESP-NOW", - "device.type.espnow.desc": "Ultra-low-latency via ESP32 gateway", - "device.type.hue": "Philips Hue", - "device.type.hue.desc": "Hue Entertainment API streaming", - "device.type.usbhid": "USB HID", - "device.type.usbhid.desc": "USB RGB peripherals (keyboards, mice)", - "device.type.spi": "SPI Direct", - "device.type.spi.desc": "Raspberry Pi GPIO/SPI LED strips", - "device.type.chroma": "Razer Chroma", - "device.type.chroma.desc": "Razer peripherals via Chroma SDK", - "device.type.gamesense": "SteelSeries", - "device.type.gamesense.desc": "SteelSeries peripherals via GameSense", - "device.chroma.device_type": "Peripheral Type:", - "device.chroma.device_type.hint": "Which Razer peripheral to control via Chroma SDK", - "device.gamesense.device_type": "Peripheral Type:", - "device.gamesense.device_type.hint": "Which SteelSeries peripheral to control via GameSense", - "device.espnow.peer_mac": "Peer MAC:", - "device.espnow.peer_mac.hint": "MAC address of the remote ESP32 receiver (e.g. AA:BB:CC:DD:EE:FF)", - "device.espnow.channel": "WiFi Channel:", - "device.espnow.channel.hint": "WiFi channel (1-14). Must match the receiver's channel.", - "device.hue.url": "Bridge IP:", - "device.hue.url.hint": "IP address of your Hue bridge", - "device.hue.username": "Bridge Username:", - "device.hue.username.hint": "Hue bridge application key from pairing", - "device.hue.client_key": "Client Key:", - "device.hue.client_key.hint": "Entertainment API client key (hex string from pairing)", - "device.hue.group_id": "Entertainment Group:", - "device.hue.group_id.hint": "Entertainment configuration ID from your Hue bridge", - "device.usbhid.url": "VID:PID:", - "device.usbhid.url.hint": "USB Vendor:Product ID in hex (e.g. 1532:0084)", - "device.spi.url": "GPIO/SPI Path:", - "device.spi.url.hint": "GPIO pin or SPI device path (e.g. spi://gpio:18)", - "device.spi.speed": "SPI Speed (Hz):", - "device.spi.speed.hint": "SPI clock speed. 800000 Hz for WS2812, 2400000 Hz for APA102.", - "device.spi.led_type": "LED Chipset:", - "device.spi.led_type.hint": "Type of addressable LED strip connected to the GPIO/SPI pin", - "device.spi.led_type.ws2812b.desc": "Most common, 800 KHz data, 3-wire RGB", - "device.spi.led_type.ws2812.desc": "Original WS2812, 800 KHz, 3-wire RGB", - "device.spi.led_type.ws2811.desc": "External driver IC, 400 KHz, 12V strips", - "device.spi.led_type.sk6812.desc": "Samsung LED, 800 KHz, 3-wire RGB", - "device.spi.led_type.sk6812_rgbw.desc": "SK6812 with dedicated white channel", - "device.gamesense.peripheral.keyboard": "Keyboard", - "device.gamesense.peripheral.keyboard.desc": "Per-key RGB illumination", - "device.gamesense.peripheral.mouse": "Mouse", - "device.gamesense.peripheral.mouse.desc": "Mouse RGB zones", - "device.gamesense.peripheral.headset": "Headset", - "device.gamesense.peripheral.headset.desc": "Headset earcup lighting", - "device.gamesense.peripheral.mousepad": "Mousepad", - "device.gamesense.peripheral.mousepad.desc": "Mousepad edge lighting zones", - "device.gamesense.peripheral.indicator": "Indicator", - "device.gamesense.peripheral.indicator.desc": "OLED/LED status indicator", - "device.dmx_protocol": "Протокол DMX:", - "device.dmx_protocol.hint": "Art-Net использует UDP порт 6454, sACN (E1.31) — UDP порт 5568", - "device.dmx_protocol.artnet.desc": "UDP unicast, порт 6454", - "device.dmx_protocol.sacn.desc": "Multicast/unicast, порт 5568", - "device.dmx_start_universe": "Начальный Universe:", - "device.dmx_start_universe.hint": "Первый DMX-юниверс (0-32767). Дополнительные юниверсы используются автоматически при >170 светодиодах.", - "device.dmx_start_channel": "Начальный канал:", - "device.dmx_start_channel.hint": "Первый DMX-канал в юниверсе (1-512)", - "device.dmx.url": "IP адрес:", - "device.dmx.url.hint": "IP адрес DMX-узла (напр. 192.168.1.50)", - "device.dmx.url.placeholder": "192.168.1.50", - "device.serial_port": "Серийный порт:", - "device.serial_port.hint": "Выберите COM порт устройства Adalight", - "device.serial_port.none": "Серийные порты не найдены", - "device.serial_port.select": "Выберите порт...", - "device.led_count_manual.hint": "Количество светодиодов на ленте (должно совпадать с вашим скетчем Arduino)", - "device.baud_rate": "Скорость порта:", - "device.baud_rate.hint": "Скорость серийного соединения. Выше = больше FPS, но требует соответствия скетчу Arduino.", - "device.led_type": "Тип LED:", - "device.led_type.hint": "RGB (3 канала) или RGBW (4 канала с выделенным белым)", - "device.send_latency": "Задержка отправки (мс):", - "device.send_latency.hint": "Имитация сетевой/серийной задержки на кадр в миллисекундах", - "device.css_processing_template": "Шаблон Обработки Полос:", - "device.css_processing_template.hint": "Шаблон обработки по умолчанию, применяемый ко всем цветовым полосам на этом устройстве", - "device.mqtt_topic": "MQTT Топик:", - "device.mqtt_topic.hint": "MQTT топик для публикации пиксельных данных (напр. mqtt://ledgrab/device/name)", - "device.mqtt_topic.placeholder": "mqtt://ledgrab/device/гостиная", - "device.ws_url": "URL подключения:", - "device.ws_url.hint": "WebSocket URL для подключения клиентов и получения LED данных", - "device.openrgb.url": "OpenRGB URL:", - "device.openrgb.url.hint": "Адрес сервера OpenRGB (напр. openrgb://localhost:6742/0)", - "device.openrgb.zone": "Зоны:", - "device.openrgb.zone.hint": "Выберите зоны LED для управления (оставьте все неотмеченными для всех зон)", - "device.openrgb.zone.loading": "Загрузка зон…", - "device.openrgb.zone.error": "Не удалось загрузить зоны", - "device.openrgb.mode": "Режим зон:", - "device.openrgb.mode.hint": "Объединённый — все зоны как одна непрерывная LED-лента. Раздельный — каждая зона независимо отображает полный эффект.", - "device.openrgb.mode.combined": "Объединённая лента", - "device.openrgb.mode.separate": "Независимые зоны", - "device.openrgb.added_multiple": "Добавлено {count} устройств", - "device.url.hint": "IP адрес или имя хоста устройства (напр. http://192.168.1.100)", - "device.name": "Имя Устройства:", - "device.name.placeholder": "ТВ в Гостиной", - "device.url": "URL:", - "device.url.placeholder": "http://192.168.1.100", - "device.led_count": "Количество Светодиодов:", - "device.led_count.hint": "Количество светодиодов, настроенных в устройстве", - "device.led_count.hint.auto": "Автоматически определяется из устройства", - "device.button.add": "Добавить Устройство", - "device.button.start": "Запустить", - "device.button.stop": "Остановить", - "device.button.settings": "Основные настройки", - "device.button.capture_settings": "Настройки захвата", - "device.button.calibrate": "Калибровка", - "device.button.remove": "Удалить", - "device.button.webui": "Открыть веб-интерфейс устройства", - "device.button.power_off": "Выключить", - "device.button.ping": "Пинг устройства", - "device.ping.online": "Онлайн ({ms}мс)", - "device.ping.offline": "Устройство недоступно", - "device.ping.error": "Ошибка пинга", - "device.power.off_success": "Устройство выключено", - "device.status.connected": "Подключено", - "device.status.disconnected": "Отключено", - "device.status.error": "Ошибка", - "device.status.processing": "Обработка", - "device.status.idle": "Ожидание", - "device.fps": "FPS:", - "device.display": "Дисплей:", - "device.remove.confirm": "Вы уверены, что хотите удалить это устройство?", - "device.added": "Устройство успешно добавлено", - "device.removed": "Устройство удалено", - "device.started": "Обработка запущена", - "device.stopped": "Обработка остановлена", - "device.metrics.actual_fps": "Факт. FPS", - "device.metrics.current_fps": "Текущ. FPS", - "device.metrics.target_fps": "Целев. FPS", - "device.metrics.potential_fps": "Потенц. FPS", - "device.metrics.frames": "Кадры", - "device.metrics.frames_skipped": "Пропущено", - "device.metrics.keepalive": "Keepalive", - "device.metrics.errors": "Ошибки", - "device.metrics.uptime": "Время работы", - "device.metrics.timing": "Тайминг пайплайна:", - "device.metrics.device_fps": "Частота обновления устройства", - "device.health.online": "Онлайн", - "device.health.offline": "Недоступен", - "device.health.streaming_unreachable": "Недоступен во время стриминга", - "device.health.checking": "Проверка...", - "device.last_seen.label": "Последний раз", - "device.last_seen.just_now": "только что", - "device.last_seen.seconds": "%d с назад", - "device.last_seen.minutes": "%d мин назад", - "device.last_seen.hours": "%d ч назад", - "device.last_seen.days": "%d д назад", - "device.tutorial.start": "Начать обучение", - "device.tip.metadata": "Информация об устройстве (кол-во LED, тип, цветовые каналы) определяется автоматически", - "device.tip.brightness": "Перетащите для регулировки яркости", - "device.tip.start": "Запуск или остановка захвата экрана", - "device.tip.settings": "Основные настройки устройства (имя, URL, интервал проверки)", - "device.tip.capture_settings": "Настройки захвата (дисплей, шаблон захвата)", - "device.tip.calibrate": "Калибровка позиций LED, направления и зоны покрытия", - "device.tip.webui": "Открыть встроенный веб-интерфейс устройства для расширенной настройки", - "device.tip.add": "Нажмите, чтобы добавить новое LED устройство", - "settings.title": "Настройки", - "settings.tab.general": "Основные", - "settings.tab.backup": "Бэкап", - "settings.tab.mqtt": "MQTT", - "settings.tab.appearance": "Оформление", - "settings.logs.open_viewer": "Открыть логи", - "settings.external_url.label": "Внешний URL", - "settings.external_url.hint": "Если указан, этот базовый URL используется в URL-ах вебхуков и других пользовательских ссылках вместо автоопределённого локального IP. Пример: https://myserver.example.com:8080", - "settings.external_url.placeholder": "https://myserver.example.com:8080", - "settings.external_url.save": "Сохранить", - "settings.external_url.saved": "Внешний URL сохранён", - "settings.external_url.save_error": "Не удалось сохранить внешний URL", - "settings.general.title": "Основные Настройки", - "settings.capture.title": "Настройки Захвата", - "settings.capture.saved": "Настройки захвата обновлены", - "settings.capture.failed": "Не удалось сохранить настройки захвата", - "settings.brightness": "Яркость:", - "settings.brightness.hint": "Общая яркость для этого устройства (0-100%)", - "settings.url.hint": "IP адрес или имя хоста устройства", - "settings.display_index": "Дисплей:", - "settings.display_index.hint": "Какой экран захватывать для этого устройства", - "settings.fps": "Целевой FPS:", - "settings.fps.hint": "Целевая частота кадров (10-90)", - "settings.capture_template": "Шаблон Движка:", - "settings.capture_template.hint": "Движок захвата экрана и конфигурация для этого устройства", - "settings.button.cancel": "Отмена", - "settings.health_interval": "Интервал Проверки (с):", - "settings.health_interval.hint": "Как часто проверять статус устройства (5-600 секунд)", - "settings.auto_shutdown": "Авто-восстановление:", - "settings.auto_shutdown.hint": "Восстанавливать устройство в режим ожидания при остановке целей или сервера", - "settings.button.save": "Сохранить Изменения", - "settings.saved": "Настройки успешно сохранены", - "settings.failed": "Не удалось сохранить настройки", - "calibration.title": "Калибровка Светодиодов", - "calibration.tip.led_count": "Укажите количество LED на каждой стороне", - "calibration.tip.start_corner": "Нажмите на угол для выбора стартовой позиции", - "calibration.tip.direction": "Переключение направления ленты (по часовой / против часовой)", - "calibration.tip.offset": "Смещение LED — расстояние от LED 0 до стартового угла", - "calibration.tip.span": "Перетащите зелёные полосы для настройки зоны покрытия", - "calibration.tip.test": "Нажмите на край для теста LED", - "calibration.tip.overlay": "Включите оверлей для отображения позиций и нумерации LED на мониторе", - "calibration.tip.toggle_inputs": "Нажмите на общее количество LED для скрытия боковых полей", - "calibration.tip.border_width": "Сколько пикселей от края экрана использовать для цветов LED", - "calibration.tip.skip_leds_start": "Пропуск LED в начале ленты — пропущенные LED остаются выключенными", - "calibration.tip.skip_leds_end": "Пропуск LED в конце ленты — пропущенные LED остаются выключенными", - "tour.welcome": "Добро пожаловать в LED Grab! Этот краткий тур познакомит вас с интерфейсом. Используйте стрелки или кнопки для навигации.", - "tour.dashboard": "Дашборд — обзор запущенных целей, автоматизаций и состояния устройств.", - "tour.targets": "Цели — добавляйте WLED-устройства, настраивайте LED-цели с захватом и калибровкой.", - "tour.sources": "Источники — управление шаблонами захвата, источниками изображений, звука и цветовых полос.", - "tour.graph": "Граф — визуальный обзор всех сущностей и их связей. Перетаскивайте порты для соединения, правый клик по связям для отключения.", - "tour.automations": "Автоматизации — автоматизируйте переключение сцен по расписанию, звуку или значениям.", - "tour.settings": "Настройки — резервное копирование и восстановление конфигурации.", - "tour.api": "API Документация — интерактивная документация REST API на базе Swagger.", - "tour.search": "Поиск — быстрый поиск и переход к любому объекту по Ctrl+K.", - "tour.theme": "Тема — переключение между тёмной и светлой темой.", - "tour.accent": "Цвет акцента — настройте цвет интерфейса по своему вкусу.", - "tour.language": "Язык — выберите предпочитаемый язык интерфейса.", - "tour.restart": "Запустить тур заново", - "tour.dash.perf": "Производительность — графики FPS в реальном времени, метрики задержки и интервал опроса.", - "tour.dash.running": "Запущенные цели — метрики стриминга и быстрая остановка.", - "tour.dash.stopped": "Остановленные цели — готовы к запуску одним нажатием.", - "tour.dash.automations": "Автоматизации — статус активных автоматизаций и быстрое включение/выключение.", - "tour.tgt.led_tab": "LED — стандартные LED-цели с настройкой устройств и цветовых полос.", - "tour.tgt.devices": "Устройства — ваши LED-контроллеры, найденные в сети.", - "tour.tgt.css": "Цветовые полосы — определите, как области экрана соответствуют сегментам LED.", - "tour.tgt.targets": "LED-цели — объедините устройство, цветовую полосу и источник захвата для стриминга.", - "tour.tgt.kc_tab": "Key Colors — альтернативный тип цели с подбором цветов вместо пиксельного маппинга.", - "tour.src.raw": "Raw — источники захвата экрана с ваших дисплеев.", - "tour.src.templates": "Шаблоны захвата — переиспользуемые конфигурации (разрешение, FPS, обрезка).", - "tour.src.static": "Статичные изображения — тестируйте настройку с файлами изображений.", - "tour.src.processed": "Обработка — применяйте эффекты: размытие, яркость, цветокоррекция.", - "tour.src.color_strip": "Цветовые полосы — определяют, как области экрана сопоставляются с LED-сегментами.", - "tour.src.audio": "Аудио — анализ микрофона или системного звука для реактивных LED-эффектов.", - "tour.src.value": "Значения — числовые источники данных для условий автоматизаций.", - "tour.src.sync": "Синхро-часы — общие таймеры для синхронизации анимаций между несколькими источниками.", - "tour.auto.list": "Автоматизации — автоматизируйте активацию сцен по времени, звуку или значениям.", - "tour.auto.add": "Нажмите + для создания новой автоматизации с условиями и сценой для активации.", - "tour.auto.card": "Каждая карточка показывает статус автоматизации, условия и кнопки управления.", - "tour.auto.scenes_list": "Сцены — сохранённые состояния системы, которые автоматизации могут активировать или вы можете применить вручную.", - "tour.auto.scenes_add": "Нажмите + для захвата текущего состояния системы как нового пресета сцены.", - "tour.auto.scenes_card": "Каждая карточка сцены показывает количество целей/устройств. Нажмите для редактирования, перезахвата или активации.", - "calibration.tutorial.start": "Начать обучение", - "calibration.overlay_toggle": "Оверлей", - "calibration.start_position": "Начальная Позиция:", - "calibration.position.bottom_left": "Нижний Левый", - "calibration.position.bottom_right": "Нижний Правый", - "calibration.position.top_left": "Верхний Левый", - "calibration.position.top_right": "Верхний Правый", - "calibration.direction": "Направление:", - "calibration.direction.clockwise": "По Часовой Стрелке", - "calibration.direction.counterclockwise": "Против Часовой Стрелки", - "calibration.leds.top": "Светодиодов Сверху:", - "calibration.leds.right": "Светодиодов Справа:", - "calibration.leds.bottom": "Светодиодов Снизу:", - "calibration.leds.left": "Светодиодов Слева:", - "calibration.offset": "Смещение LED:", - "calibration.offset.hint": "Расстояние от физического LED 0 до стартового угла (по направлению ленты)", - "calibration.skip_start": "Пропуск LED (начало):", - "calibration.skip_start.hint": "Количество LED, которые будут выключены в начале ленты (0 = нет)", - "calibration.skip_end": "Пропуск LED (конец):", - "calibration.skip_end.hint": "Количество LED, которые будут выключены в конце ленты (0 = нет)", - "calibration.border_width": "Граница (px):", - "calibration.border_width.hint": "Сколько пикселей от края экрана выбирать для цвета LED (1-100)", - "calibration.button.cancel": "Отмена", - "calibration.button.save": "Сохранить", - "calibration.saved": "Калибровка сохранена", - "calibration.failed": "Не удалось сохранить калибровку", - "server.healthy": "Сервер онлайн", - "server.offline": "Сервер офлайн", - "error.unauthorized": "Не авторизован - пожалуйста, войдите", - "error.network": "Сетевая ошибка", - "error.unknown": "Произошла ошибка", - "modal.discard_changes": "У вас есть несохранённые изменения. Отменить их?", - "confirm.title": "Подтверждение", - "confirm.yes": "Да", - "confirm.no": "Нет", - "confirm.stop_all": "Остановить все запущенные цели?", - "confirm.turn_off_device": "Выключить это устройство?", - "common.loading": "Загрузка...", - "common.delete": "Удалить", - "common.remove": "Убрать", - "common.edit": "Редактировать", - "common.clone": "Клонировать", - "common.none": "Нет", - "common.none_no_cspt": "Нет (без шаблона обработки)", - "common.none_no_input": "Нет (без источника)", - "common.none_own_speed": "Нет (своя скорость)", - "common.undo": "Отменить", - "validation.required": "Обязательное поле", - "bulk.processing": "Обработка…", - "api.error.timeout": "Превышено время ожидания — попробуйте снова", - "api.error.network": "Ошибка сети — проверьте подключение", - "palette.search": "Поиск…", - "section.filter.placeholder": "Фильтр...", - "section.filter.reset": "Очистить фильтр", - "tags.label": "Теги", - "tags.hint": "Назначьте теги для группировки и фильтрации карточек", - "tags.placeholder": "Добавить тег...", - "section.expand_all": "Развернуть все секции", - "section.collapse_all": "Свернуть все секции", - "streams.title": "Источники", - "streams.description": "Источники определяют конвейер захвата. Сырой источник захватывает экран с помощью шаблона захвата. Обработанный источник применяет постобработку к другому источнику. Назначайте источники устройствам.", - "streams.group.raw": "Источники", - "streams.group.raw_templates": "Шаблоны движка", - "streams.group.processed": "Источники", - "streams.group.proc_templates": "Шаблоны фильтров", - "streams.group.css_processing": "Шаблоны Обработки", - "streams.group.color_strip": "Цветовые Полосы", - "streams.group.audio": "Аудио", - "streams.group.audio_templates": "Аудио шаблоны", - "streams.section.streams": "Источники", - "streams.add": "Добавить Источник", - "streams.add.raw": "Добавить Захват Экрана", - "streams.add.processed": "Добавить Обработанный", - "streams.edit": "Редактировать Источник", - "streams.edit.raw": "Редактировать Захват Экрана", - "streams.edit.processed": "Редактировать Обработанный Источник", - "streams.name": "Имя Источника:", - "streams.name.placeholder": "Мой Источник", - "streams.type": "Тип:", - "streams.type.raw": "Захват экрана", - "streams.type.processed": "Обработанный", - "streams.display": "Дисплей:", - "streams.display.hint": "Какой экран захватывать", - "streams.capture_template": "Шаблон Движка:", - "streams.capture_template.hint": "Шаблон движка, определяющий способ захвата экрана", - "streams.target_fps": "Целевой FPS:", - "streams.target_fps.hint": "Целевое количество кадров в секунду (1-90)", - "streams.source": "Источник:", - "streams.source.hint": "Источник, к которому применяются фильтры обработки", - "streams.pp_template": "Шаблон Фильтра:", - "streams.pp_template.hint": "Шаблон фильтра для применения к источнику", - "streams.description_label": "Описание (необязательно):", - "streams.description_placeholder": "Опишите этот источник...", - "streams.created": "Источник успешно создан", - "streams.updated": "Источник успешно обновлён", - "streams.deleted": "Источник успешно удалён", - "streams.delete.confirm": "Вы уверены, что хотите удалить этот источник?", - "streams.modal.loading": "Загрузка...", - "streams.error.load": "Не удалось загрузить источники", - "streams.error.required": "Пожалуйста, заполните все обязательные поля", - "streams.error.delete": "Не удалось удалить источник", - "streams.test.title": "Тест Источника", - "streams.test.run": "Запустить", - "streams.test.running": "Тестирование источника...", - "streams.test.duration": "Длительность Захвата (с):", - "streams.test.error.failed": "Тест источника не удался", - "postprocessing.title": "Шаблоны Фильтров", - "postprocessing.description": "Шаблоны обработки определяют фильтры изображений и цветокоррекцию. Назначайте их обработанным источникам для единообразной постобработки на всех устройствах.", - "postprocessing.add": "Добавить Шаблон Фильтра", - "postprocessing.edit": "Редактировать Шаблон Фильтра", - "postprocessing.name": "Имя Шаблона:", - "postprocessing.name.placeholder": "Мой Шаблон Фильтра", - "filters.select_type": "Выберите тип фильтра...", - "filters.add": "Добавить фильтр", - "filters.remove": "Удалить", - "filters.drag_to_reorder": "Перетащите для изменения порядка", - "filters.empty": "Фильтры не добавлены. Используйте селектор ниже для добавления.", - "filters.brightness": "Яркость", - "filters.brightness.desc": "Регулировка общей яркости изображения", - "filters.saturation": "Насыщенность", - "filters.saturation.desc": "Усиление или снижение интенсивности цвета", - "filters.gamma": "Гамма", - "filters.gamma.desc": "Нелинейная коррекция кривой яркости", - "filters.downscaler": "Уменьшение", - "filters.downscaler.desc": "Снижение разрешения для быстрой обработки", - "filters.pixelate": "Пикселизация", - "filters.pixelate.desc": "Мозаичное усреднение блоков", - "filters.auto_crop": "Авто Обрезка", - "filters.auto_crop.desc": "Удаление чёрных полос из леттербокса", - "filters.flip": "Отражение", - "filters.flip.desc": "Зеркальное отражение по горизонтали или вертикали", - "filters.color_correction": "Цветокоррекция", - "filters.color_correction.desc": "Баланс белого и цветовая температура", - "filters.filter_template": "Шаблон фильтров", - "filters.filter_template.desc": "Встроить другой шаблон обработки", - "filters.css_filter_template": "Шаблон Фильтра Полос", - "filters.css_filter_template.desc": "Встроить другой шаблон обработки полос", - "filters.frame_interpolation": "Интерполяция кадров", - "filters.frame_interpolation.desc": "Сглаживание между кадрами", - "filters.noise_gate": "Шумоподавление", - "filters.noise_gate.desc": "Подавление малых изменений цвета ниже порога", - "filters.palette_quantization": "Квантизация палитры", - "filters.palette_quantization.desc": "Сокращение цветов до ограниченной палитры", - "filters.reverse": "Реверс", - "filters.reverse.desc": "Изменить порядок светодиодов на обратный", - "postprocessing.description_label": "Описание (необязательно):", - "postprocessing.description_placeholder": "Опишите этот шаблон...", - "postprocessing.created": "Шаблон успешно создан", - "postprocessing.updated": "Шаблон успешно обновлён", - "postprocessing.deleted": "Шаблон успешно удалён", - "postprocessing.delete.confirm": "Вы уверены, что хотите удалить этот шаблон фильтра?", - "postprocessing.error.load": "Не удалось загрузить шаблоны фильтров", - "postprocessing.error.required": "Пожалуйста, заполните все обязательные поля", - "postprocessing.error.delete": "Не удалось удалить шаблон фильтра", - "postprocessing.config.show": "Показать настройки", - "postprocessing.test.title": "Тест шаблона фильтра", - "postprocessing.test.source_stream": "Источник:", - "postprocessing.test.running": "Тестирование шаблона фильтра...", - "postprocessing.test.error.no_stream": "Пожалуйста, выберите источник", - "postprocessing.test.error.failed": "Тест шаблона фильтра не удался", - "css_processing.title": "Шаблоны Обработки Полос", - "css_processing.add": "Добавить Шаблон Обработки Полос", - "css_processing.edit": "Редактировать Шаблон Обработки Полос", - "css_processing.name": "Имя Шаблона:", - "css_processing.name_placeholder": "Мой Шаблон Обработки Полос", - "css_processing.description_label": "Описание (необязательно):", - "css_processing.description_placeholder": "Опишите этот шаблон...", - "css_processing.created": "Шаблон обработки полос создан", - "css_processing.updated": "Шаблон обработки полос обновлён", - "css_processing.deleted": "Шаблон обработки полос удалён", - "css_processing.delete.confirm": "Вы уверены, что хотите удалить этот шаблон обработки полос?", - "css_processing.error.required": "Заполните все обязательные поля", - "css_processing.error.load": "Ошибка загрузки шаблона обработки полос", - "css_processing.error.delete": "Ошибка удаления шаблона обработки полос", - "css_processing.error.clone_failed": "Не удалось клонировать шаблон обработки полос", - "device.button.stream_selector": "Настройки источника", - "device.stream_settings.title": "Настройки источника", - "device.stream_selector.label": "Источник:", - "device.stream_selector.hint": "Выберите источник, определяющий что это устройство захватывает и обрабатывает", - "device.stream_selector.none": "-- Источник не назначен --", - "device.stream_selector.saved": "Настройки источника обновлены", - "device.stream_settings.border_width": "Ширина границы (px):", - "device.stream_settings.border_width_hint": "Сколько пикселей от края экрана выбирать для цвета LED (1-100)", - "device.stream_settings.interpolation": "Режим интерполяции:", - "device.stream_settings.interpolation.average": "Среднее", - "device.stream_settings.interpolation.median": "Медиана", - "device.stream_settings.interpolation.dominant": "Доминантный", - "device.stream_settings.interpolation_hint": "Как вычислять цвет LED из выбранных пикселей", - "device.stream_settings.smoothing": "Сглаживание:", - "device.stream_settings.smoothing_hint": "Временное смешивание между кадрами (0=нет, 1=полное). Уменьшает мерцание.", - "device.tip.stream_selector": "Настройки источника и проекции LED для этого устройства", - "streams.group.static_image": "Статические", - "streams.add.static_image": "Добавить статическое изображение (источник)", - "streams.edit.static_image": "Редактировать статическое изображение (источник)", - "streams.type.static_image": "Статическое изображение", - "streams.group.video": "Видео", - "streams.add.video": "Добавить видеоисточник", - "streams.edit.video": "Редактировать видеоисточник", - "picture_source.type.video": "Видео", - "picture_source.type.video.desc": "Потоковые кадры из загруженного видео", - "picture_source.video.loop": "Зацикливание:", - "picture_source.video.speed": "Скорость воспроизведения:", - "picture_source.video.start_time": "Время начала (с):", - "picture_source.video.end_time": "Время окончания (с):", - "picture_source.video.resolution_limit": "Макс. ширина (px):", - "picture_source.video.resolution_limit.hint": "Уменьшение видео при декодировании для производительности", - "streams.image_asset": "Изображение:", - "streams.image_asset.select": "Выберите изображение…", - "streams.image_asset.search": "Поиск изображений…", - "streams.video_asset": "Видео:", - "streams.video_asset.select": "Выберите видео…", - "streams.video_asset.search": "Поиск видео…", - "targets.title": "Цели", - "targets.description": "Цели связывают источники цветовых полос с устройствами вывода. Каждая цель ссылается на устройство и источник цветовой полосы.", - "targets.subtab.wled": "LED", - "targets.subtab.led": "LED", - "targets.section.devices": "Устройства", - "targets.section.color_strips": "Источники цветовых полос", - "targets.section.targets": "Цели", - "targets.section.specific_settings": "Специальные настройки", - "targets.section.advanced": "Расширенные", - "targets.add": "Добавить Цель", - "targets.edit": "Редактировать Цель", - "targets.loading": "Загрузка целей...", - "targets.none": "Цели не настроены", - "targets.failed": "Не удалось загрузить цели", - "targets.name": "Имя Цели:", - "targets.name.placeholder": "Моя Цель", - "targets.device": "Устройство:", - "targets.device.hint": "Выберите LED устройство для передачи данных", - "targets.device.none": "-- Выберите устройство --", - "targets.color_strip_source": "Источник цветовой полосы:", - "targets.color_strip_source.hint": "Выберите источник цветовой полосы, который предоставляет цвета LED для этой цели", - "targets.no_css": "Нет источника", - "targets.source": "Источник:", - "targets.source.hint": "Какой источник изображения захватывать и обрабатывать", - "targets.source.none": "-- Источник не назначен --", - "targets.metrics.pipeline": "Детали конвейера", - "targets.fps": "Целевой FPS:", - "targets.fps.hint": "Целевая частота кадров для захвата и обновления LED (1-90)", - "targets.fps.rec": "Макс. аппаратный ≈ {fps} fps ({leds} LED)", - "targets.border_width": "Ширина границы (px):", - "targets.border_width.hint": "Сколько пикселей от края экрана выбирать для цвета LED (1-100)", - "targets.interpolation": "Режим интерполяции:", - "targets.interpolation.hint": "Как вычислять цвет LED из выбранных пикселей", - "targets.interpolation.average": "Среднее", - "targets.interpolation.median": "Медиана", - "targets.interpolation.dominant": "Доминантный", - "targets.smoothing": "Сглаживание:", - "targets.smoothing.hint": "Временное смешивание между кадрами (0=нет, 1=полное). Уменьшает мерцание.", - "targets.keepalive_interval": "Интервал поддержания связи:", - "targets.keepalive_interval.hint": "Как часто повторно отправлять последний кадр при статичном источнике для удержания устройства в режиме live (0.5-5.0с)", - "targets.created": "Цель успешно создана", - "targets.updated": "Цель успешно обновлена", - "targets.deleted": "Цель успешно удалена", - "targets.delete.confirm": "Вы уверены, что хотите удалить эту цель?", - "targets.error.load": "Не удалось загрузить цели", - "targets.error.required": "Пожалуйста, заполните все обязательные поля", - "targets.error.name_required": "Введите название цели", - "targets.error.delete": "Не удалось удалить цель", - "targets.button.start": "Запустить", - "targets.button.stop": "Остановить", - "targets.status.processing": "Обработка", - "targets.status.idle": "Ожидание", - "targets.status.error": "Ошибка", - "targets.metrics.actual_fps": "Факт. FPS", - "targets.metrics.target_fps": "Целев. FPS", - "targets.metrics.frames": "Кадры", - "targets.metrics.errors": "Ошибки", - "targets.subtab.key_colors": "Ключевые Цвета", - "targets.section.key_colors": "Цели Ключевых Цветов", - "kc.add": "Добавить Цель Ключевых Цветов", - "kc.edit": "Редактировать Цель Ключевых Цветов", - "kc.name": "Имя Цели:", - "kc.name.placeholder": "Моя Цель Ключевых Цветов", - "kc.source": "Источник:", - "kc.source.hint": "Из какого источника извлекать цвета", - "kc.source.none": "-- Источник не назначен --", - "kc.fps": "FPS Извлечения:", - "kc.fps.hint": "Сколько раз в секунду извлекать цвета (1-60)", - "kc.interpolation": "Режим Цвета:", - "kc.interpolation.hint": "Как вычислять ключевой цвет из пикселей в каждом прямоугольнике", - "kc.interpolation.average": "Среднее", - "kc.interpolation.median": "Медиана", - "kc.interpolation.dominant": "Доминантный", - "kc.interpolation.average.desc": "Среднее всех цветов пикселей", - "kc.interpolation.median.desc": "Медианное значение по каналам", - "kc.interpolation.dominant.desc": "Наиболее частый цвет", - "kc.smoothing": "Сглаживание:", - "kc.smoothing.hint": "Временное смешивание между извлечениями (0=нет, 1=полное)", - "kc.pattern_template": "Шаблон Паттерна:", - "kc.pattern_template.hint": "Выберите шаблон прямоугольников для извлечения цветов", - "kc.pattern_template.none": "-- Выберите шаблон паттерна --", - "kc.brightness_vs": "Источник Яркости:", - "kc.brightness_vs.hint": "Опциональный источник значений, динамически управляющий яркостью каждый кадр (умножается на ручной слайдер яркости)", - "kc.brightness_vs.none": "Нет (только ручная яркость)", - "kc.created": "Цель ключевых цветов успешно создана", - "kc.updated": "Цель ключевых цветов успешно обновлена", - "kc.deleted": "Цель ключевых цветов успешно удалена", - "kc.delete.confirm": "Вы уверены, что хотите удалить эту цель ключевых цветов?", - "kc.error.no_pattern": "Пожалуйста, выберите шаблон паттерна", - "kc.error.required": "Пожалуйста, заполните все обязательные поля", - "kc.colors.none": "Цвета пока не извлечены", - "kc.test": "Тест", - "kc.test.error": "Ошибка теста", - "targets.section.pattern_templates": "Шаблоны Паттернов", - "pattern.add": "Добавить Шаблон Паттерна", - "pattern.edit": "Редактировать Шаблон Паттерна", - "pattern.name": "Имя Шаблона:", - "pattern.name.placeholder": "Мой Шаблон Паттерна", - "pattern.description_label": "Описание (необязательно):", - "pattern.description_placeholder": "Опишите этот паттерн...", - "pattern.rectangles": "Прямоугольники", - "pattern.rect.name": "Имя", - "pattern.rect.x": "X", - "pattern.rect.y": "Y", - "pattern.rect.width": "Ш", - "pattern.rect.height": "В", - "pattern.rect.add": "Добавить Прямоугольник", - "pattern.rect.remove": "Удалить", - "pattern.rect.empty": "Прямоугольники не определены. Добавьте хотя бы один.", - "pattern.created": "Шаблон паттерна успешно создан", - "pattern.updated": "Шаблон паттерна успешно обновлён", - "pattern.deleted": "Шаблон паттерна успешно удалён", - "pattern.delete.confirm": "Вы уверены, что хотите удалить этот шаблон паттерна?", - "pattern.delete.referenced": "Невозможно удалить: шаблон используется целью", - "pattern.error.required": "Пожалуйста, заполните все обязательные поля", - "pattern.visual_editor": "Визуальный Редактор", - "pattern.capture_bg": "Захватить Фон", - "pattern.source_for_bg": "Источник для Фона:", - "pattern.source_for_bg.none": "-- Выберите источник --", - "pattern.delete_selected": "Удалить Выбранный", - "pattern.name.hint": "Описательное имя для этой раскладки прямоугольников", - "pattern.description.hint": "Необязательные заметки о назначении этого паттерна", - "pattern.visual_editor.hint": "Нажмите кнопки + чтобы добавить прямоугольники. Тяните края для изменения размера, тяните внутри для перемещения.", - "pattern.rectangles.hint": "Точная настройка позиций и размеров прямоугольников в координатах (0.0 до 1.0)", - "overlay.toggle": "Переключить наложение на экран", - "overlay.button.show": "Показать визуализацию наложения", - "overlay.button.hide": "Скрыть визуализацию наложения", - "overlay.started": "Визуализация наложения запущена", - "overlay.stopped": "Визуализация наложения остановлена", - "overlay.error.start": "Не удалось запустить наложение", - "overlay.error.stop": "Не удалось остановить наложение", - "dashboard.title": "Обзор", - "dashboard.section.targets": "Цели", - "dashboard.section.running": "Запущенные", - "dashboard.section.stopped": "Остановленные", - "dashboard.no_targets": "Нет настроенных целей", - "dashboard.uptime": "Время работы", - "dashboard.fps": "FPS", - "dashboard.errors": "Ошибки", - "dashboard.device": "Устройство", - "dashboard.stop_all": "Остановить все", - "dashboard.failed": "Не удалось загрузить обзор", - "dashboard.section.automations": "Автоматизации", - "dashboard.section.scenes": "Пресеты сцен", - "dashboard.section.sync_clocks": "Синхронные часы", - "dashboard.targets": "Цели", - "dashboard.section.performance": "Производительность системы", - "dashboard.perf.cpu": "ЦП", - "dashboard.perf.ram": "ОЗУ", - "dashboard.perf.gpu": "ГП", - "dashboard.perf.unavailable": "недоступно", - "dashboard.perf.color": "Цвет графика", - "dashboard.perf.mode.system": "Система", - "dashboard.perf.mode.app": "Приложение", - "dashboard.perf.mode.both": "Оба", - "dashboard.poll_interval": "Интервал обновления", - "automations.title": "Автоматизации", - "automations.empty": "Автоматизации не настроены. Создайте автоматизацию для автоматической активации сцен.", - "automations.add": "Добавить автоматизацию", - "automations.edit": "Редактировать автоматизацию", - "automations.delete.confirm": "Удалить автоматизацию \"{name}\"?", - "automations.name": "Название:", - "automations.name.hint": "Описательное имя для автоматизации", - "automations.name.placeholder": "Моя автоматизация", - "automations.enabled": "Включена:", - "automations.enabled.hint": "Отключённые автоматизации не активируются даже при выполнении условий", - "automations.condition_logic": "Логика условий:", - "automations.condition_logic.hint": "Как объединяются несколько условий: ЛЮБОЕ (ИЛИ) или ВСЕ (И)", - "automations.condition_logic.or": "Любое условие (ИЛИ)", - "automations.condition_logic.and": "Все условия (И)", - "automations.condition_logic.or.desc": "Срабатывает при любом совпадении", - "automations.condition_logic.and.desc": "Срабатывает только при всех", - "automations.conditions": "Условия:", - "automations.conditions.hint": "Правила, определяющие когда автоматизация активируется", - "automations.conditions.add": "Добавить условие", - "automations.conditions.empty": "Нет условий — автоматизация всегда активна когда включена", - "automations.condition.always": "Всегда", - "automations.condition.always.desc": "Всегда активно", - "automations.condition.always.hint": "Автоматизация активируется сразу при включении и остаётся активной.", - "automations.condition.startup": "Автозапуск", - "automations.condition.startup.desc": "При запуске сервера", - "automations.condition.startup.hint": "Активируется при запуске сервера и остаётся активной пока включена.", - "automations.condition.application": "Приложение", - "automations.condition.application.desc": "Приложение запущено", - "automations.condition.application.apps": "Приложения:", - "automations.condition.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)", - "automations.condition.application.browse": "Обзор", - "automations.condition.application.search": "Фильтр процессов...", - "automations.condition.application.no_processes": "Процессы не найдены", - "automations.condition.application.match_type": "Тип соответствия:", - "automations.condition.application.match_type.hint": "Как определять наличие приложения", - "automations.condition.application.match_type.running": "Запущено", - "automations.condition.application.match_type.running.desc": "Процесс активен", - "automations.condition.application.match_type.topmost": "На переднем плане", - "automations.condition.application.match_type.topmost.desc": "Окно в фокусе", - "automations.condition.application.match_type.topmost_fullscreen": "Передний план + ПЭ", - "automations.condition.application.match_type.topmost_fullscreen.desc": "В фокусе + полный экран", - "automations.condition.application.match_type.fullscreen": "Полный экран", - "automations.condition.application.match_type.fullscreen.desc": "Любое полноэкранное", - "automations.condition.time_of_day": "Время суток", - "automations.condition.time_of_day.desc": "Диапазон времени", - "automations.condition.time_of_day.start_time": "Время начала:", - "automations.condition.time_of_day.end_time": "Время окончания:", - "automations.condition.time_of_day.overnight_hint": "Для ночных диапазонов (например 22:00–06:00) укажите время начала позже времени окончания.", - "automations.condition.system_idle": "Бездействие системы", - "automations.condition.system_idle.desc": "Бездействие/активность", - "automations.condition.system_idle.idle_minutes": "Тайм-аут бездействия (минуты):", - "automations.condition.system_idle.mode": "Режим срабатывания:", - "automations.condition.system_idle.when_idle": "При бездействии", - "automations.condition.system_idle.when_active": "При активности", - "automations.condition.display_state": "Состояние дисплея", - "automations.condition.display_state.desc": "Монитор вкл/выкл", - "automations.condition.display_state.state": "Состояние монитора:", - "automations.condition.display_state.on": "Включён", - "automations.condition.display_state.off": "Выключен (спящий режим)", - "automations.condition.mqtt": "MQTT", - "automations.condition.mqtt.desc": "MQTT сообщение", - "automations.condition.mqtt.topic": "Топик:", - "automations.condition.mqtt.payload": "Значение:", - "automations.condition.mqtt.match_mode": "Режим сравнения:", - "automations.condition.mqtt.match_mode.exact": "Точное совпадение", - "automations.condition.mqtt.match_mode.contains": "Содержит", - "automations.condition.mqtt.match_mode.regex": "Регулярное выражение", - "automations.condition.mqtt.hint": "Активировать при получении совпадающего значения по MQTT топику", - "automations.condition.webhook": "Вебхук", - "automations.condition.webhook.desc": "HTTP вызов", - "automations.condition.webhook.hint": "Активировать через HTTP-запрос от внешних сервисов (Home Assistant, IFTTT, curl и т.д.)", - "automations.condition.webhook.url": "URL вебхука:", - "automations.condition.webhook.copy": "Скопировать", - "automations.condition.webhook.copied": "Скопировано!", - "automations.condition.webhook.save_first": "Сначала сохраните автоматизацию для генерации URL вебхука", - "automations.scene": "Сцена:", - "automations.scene.hint": "Пресет сцены для активации при выполнении условий", - "automations.scene.search_placeholder": "Поиск сцен...", - "automations.scene.none_selected": "Нет (без сцены)", - "automations.scene.none_available": "Нет доступных сцен", - "automations.deactivation_mode": "Деактивация:", - "automations.deactivation_mode.hint": "Что происходит, когда условия перестают выполняться", - "automations.deactivation_mode.none": "Ничего", - "automations.deactivation_mode.none.desc": "Оставить текущее состояние", - "automations.deactivation_mode.revert": "Откатить", - "automations.deactivation_mode.revert.desc": "Вернуть предыдущее состояние", - "automations.deactivation_mode.fallback_scene": "Резервная", - "automations.deactivation_mode.fallback_scene.desc": "Активировать резервную сцену", - "automations.deactivation_scene": "Резервная сцена:", - "automations.deactivation_scene.hint": "Сцена для активации при деактивации автоматизации", - "automations.status.active": "Активна", - "automations.status.inactive": "Неактивна", - "automations.status.disabled": "Отключена", - "automations.action.disable": "Отключить", - "automations.last_activated": "Последняя активация", - "automations.logic.and": " И ", - "automations.logic.or": " ИЛИ ", - "automations.logic.all": "ВСЕ", - "automations.logic.any": "ЛЮБОЕ", - "automations.updated": "Автоматизация обновлена", - "automations.created": "Автоматизация создана", - "automations.deleted": "Автоматизация удалена", - "automations.error.name_required": "Введите название", - "automations.error.clone_failed": "Не удалось клонировать автоматизацию", - "scenes.title": "Сцены", - "scenes.add": "Захватить сцену", - "scenes.edit": "Редактировать сцену", - "scenes.name": "Название:", - "scenes.name.hint": "Описательное имя для этого пресета сцены", - "scenes.name.placeholder": "Моя сцена", - "scenes.description": "Описание:", - "scenes.description.hint": "Необязательное описание назначения этой сцены", - "scenes.targets": "Цели:", - "scenes.targets.hint": "Выберите какие цели включить в снимок сцены", - "scenes.targets.add": "Добавить цель", - "scenes.targets.search_placeholder": "Поиск целей...", - "scenes.capture": "Захват", - "scenes.activate": "Активировать сцену", - "scenes.recapture": "Перезахватить текущее состояние", - "scenes.delete": "Удалить сцену", - "scenes.targets_count": "целей", - "scenes.captured": "Сцена захвачена", - "scenes.updated": "Сцена обновлена", - "scenes.activated": "Сцена активирована", - "scenes.activated_partial": "Сцена активирована частично", - "scenes.errors": "ошибок", - "scenes.recaptured": "Сцена перезахвачена", - "scenes.deleted": "Сцена удалена", - "scenes.recapture_confirm": "Перезахватить текущее состояние в \"{name}\"?", - "scenes.delete_confirm": "Удалить сцену \"{name}\"?", - "scenes.error.name_required": "Необходимо указать название", - "scenes.error.save_failed": "Не удалось сохранить сцену", - "scenes.error.activate_failed": "Не удалось активировать сцену", - "scenes.error.recapture_failed": "Не удалось перезахватить сцену", - "scenes.error.delete_failed": "Не удалось удалить сцену", - "scenes.cloned": "Сцена клонирована", - "scenes.error.clone_failed": "Не удалось клонировать сцену", - "time.hours_minutes": "{h}ч {m}м", - "time.minutes_seconds": "{m}м {s}с", - "time.seconds": "{s}с", - "dashboard.type.led": "LED", - "dashboard.type.kc": "Цвета клавиш", - "aria.close": "Закрыть", - "aria.save": "Сохранить", - "aria.cancel": "Отмена", - "aria.previous": "Назад", - "aria.next": "Вперёд", - "aria.hint": "Показать подсказку", - "color_strip.select_type": "Выберите тип цветовой полосы", - "color_strip.add": "Добавить", - "color_strip.edit": "Редактировать", - "color_strip.name": "Название:", - "color_strip.name.placeholder": "Настенная полоса", - "color_strip.picture_source": "Источник изображения:", - "color_strip.picture_source.hint": "Источник захвата экрана для расчёта цветов светодиодов", - "color_strip.fps": "Целевой FPS:", - "color_strip.fps.hint": "Целевая частота кадров для обновления цветов светодиодов (10-90)", - "color_strip.interpolation": "Режим цвета:", - "color_strip.interpolation.hint": "Как вычислять цвет светодиода по пикселям рамки", - "color_strip.interpolation.average": "Среднее", - "color_strip.interpolation.median": "Медиана", - "color_strip.interpolation.dominant": "Доминирующий", - "color_strip.interpolation.average.desc": "Смешивает все пиксели в усреднённый цвет", - "color_strip.interpolation.median.desc": "Берёт средний цвет, игнорируя выбросы", - "color_strip.interpolation.dominant.desc": "Использует самый частый цвет в выборке", - "color_strip.smoothing": "Сглаживание:", - "color_strip.smoothing.hint": "Временное смешивание кадров (0=без смешивания, 1=полное). Уменьшает мерцание.", - "color_strip.frame_interpolation": "Интерполяция кадров:", - "color_strip.frame_interpolation.hint": "Смешивает последовательные захваченные кадры для вывода на полной целевой частоте кадров, даже если скорость захвата ниже. Уменьшает заметные ступеньки при плавных переходах.", - "color_strip.color_corrections": "Цветокоррекция", - "color_strip.brightness": "Яркость:", - "color_strip.brightness.hint": "Множитель яркости (0=выкл, 1=без изменений, 2=двойная). Применяется после извлечения цвета.", - "color_strip.saturation": "Насыщенность:", - "color_strip.saturation.hint": "Насыщенность цвета (0=оттенки серого, 1=без изменений, 2=двойная насыщенность)", - "color_strip.gamma": "Гамма:", - "color_strip.gamma.hint": "Гамма-коррекция (1=без коррекции, <1=ярче средние тона, >1=темнее средние тона)", - "color_strip.test_device": "Тестировать на устройстве:", - "color_strip.test_device.hint": "Выберите устройство для отправки тестовых пикселей при нажатии на рамку", - "color_strip.leds": "Количество светодиодов", - "color_strip.led_count": "Количество LED:", - "color_strip.led_count.hint": "Общее число светодиодов на физической полосе. Для источников экрана: 0 = автоматически из калибровки (светодиоды за ТВ будут чёрными). Для статического цвета: укажите точное количество светодиодов устройства.", - "color_strip.created": "Источник цветовой полосы создан", - "color_strip.updated": "Источник цветовой полосы обновлён", - "color_strip.deleted": "Источник цветовой полосы удалён", - "color_strip.delete.confirm": "Удалить этот источник цветовой полосы?", - "color_strip.delete.referenced": "Невозможно удалить: источник используется в цели", - "color_strip.error.name_required": "Введите название", - "color_strip.type": "Тип:", - "color_strip.type.hint": "Источник изображения получает цвета светодиодов из захвата экрана. Статический цвет заполняет все светодиоды одним постоянным цветом. Градиент распределяет цветовой градиент по всем светодиодам. Смена цвета плавно циклически переключается между заданными цветами. Композит накладывает несколько источников как смешанные слои. Аудиореактив управляет LED от аудиосигнала в реальном времени. API-ввод принимает массивы цветов LED от внешних клиентов через REST или WebSocket.", - "color_strip.type.picture": "Источник изображения", - "color_strip.type.picture.desc": "Цвета из захвата экрана", - "color_strip.type.picture_advanced": "Мультимонитор", - "color_strip.type.picture_advanced.desc": "Калибровка линиями по нескольким мониторам", - "color_strip.type.static": "Статический цвет", - "color_strip.type.static.desc": "Заливка одним цветом", - "color_strip.type.gradient": "Градиент", - "color_strip.type.gradient.desc": "Плавный переход цветов по ленте", - "color_strip.type.color_cycle": "Смена цвета", - "color_strip.type.color_cycle.desc": "Циклическая смена списка цветов", - "color_strip.static_color": "Цвет:", - "color_strip.static_color.hint": "Статический цвет, который будет отправлен на все светодиоды полосы.", - "color_strip.gradient.preview": "Градиент:", - "color_strip.gradient.preview.hint": "Предпросмотр градиента. Нажмите на дорожку маркеров чтобы добавить остановку. Перетащите маркеры для изменения позиции.", - "color_strip.gradient.stops": "Цветовые остановки:", - "color_strip.gradient.stops.hint": "Каждая остановка задаёт цвет в относительной позиции (0.0 = начало, 1.0 = конец). Кнопка ↔ добавляет цвет справа для создания резкого перехода.", - "color_strip.gradient.stops_count": "остановок", - "color_strip.gradient.add_stop": "+ Добавить", - "color_strip.gradient.position": "Позиция (0.0–1.0)", - "color_strip.gradient.bidir.hint": "Добавить второй цвет справа от этой остановки для создания резкого перехода в градиенте.", - "color_strip.gradient.min_stops": "Градиент должен содержать не менее 2 остановок", - "color_strip.gradient.preset": "Пресет:", - "color_strip.gradient.preset.hint": "Загрузить готовую палитру градиента. Выбор пресета заменяет текущие остановки.", - "color_strip.gradient.preset.custom": "— Свой —", - "color_strip.gradient.preset.rainbow": "Радуга", - "color_strip.gradient.preset.sunset": "Закат", - "color_strip.gradient.preset.ocean": "Океан", - "color_strip.gradient.preset.forest": "Лес", - "color_strip.gradient.preset.fire": "Огонь", - "color_strip.gradient.preset.lava": "Лава", - "color_strip.gradient.preset.aurora": "Аврора", - "color_strip.gradient.preset.ice": "Лёд", - "color_strip.gradient.preset.warm": "Тёплый", - "color_strip.gradient.preset.cool": "Холодный", - "color_strip.gradient.preset.neon": "Неон", - "color_strip.gradient.preset.pastel": "Пастельный", - "color_strip.gradient.preset.save_button": "Сохранить как пресет…", - "color_strip.gradient.preset.save_prompt": "Введите название пресета:", - "color_strip.gradient.preset.saved": "Пресет сохранён", - "color_strip.gradient.preset.deleted": "Пресет удалён", - "color_strip.gradient.preset.apply": "Применить", - "color_strip.animation": "Анимация", - "color_strip.animation.type": "Эффект:", - "color_strip.animation.type.hint": "Эффект анимации.", - "color_strip.animation.type.none": "Нет (без эффекта анимации)", - "color_strip.animation.type.none.desc": "Статичные цвета без анимации", - "color_strip.animation.type.breathing": "Дыхание", - "color_strip.animation.type.breathing.desc": "Плавное угасание и нарастание яркости", - "color_strip.animation.type.color_cycle": "Смена цвета", - "color_strip.animation.type.gradient_shift": "Сдвиг градиента", - "color_strip.animation.type.gradient_shift.desc": "Сдвигает градиент вдоль ленты", - "color_strip.animation.type.wave": "Волна", - "color_strip.animation.type.wave.desc": "Синусоидальная волна яркости вдоль ленты", - "color_strip.animation.type.strobe": "Стробоскоп", - "color_strip.animation.type.strobe.desc": "Быстрое мигание вкл/выкл", - "color_strip.animation.type.sparkle": "Искры", - "color_strip.animation.type.sparkle.desc": "Случайные светодиоды кратковременно вспыхивают", - "color_strip.animation.type.pulse": "Пульс", - "color_strip.animation.type.pulse.desc": "Резкая вспышка яркости с быстрым затуханием", - "color_strip.animation.type.candle": "Свеча", - "color_strip.animation.type.candle.desc": "Тёплое мерцание, как у свечи", - "color_strip.animation.type.rainbow_fade": "Радужный перелив", - "color_strip.animation.type.rainbow_fade.desc": "Циклический переход по всему спектру оттенков", - "color_strip.animation.speed": "Скорость:", - "color_strip.animation.speed.hint": "Множитель скорости анимации. 1.0 ≈ один цикл в секунду для дыхания; большие значения ускоряют анимацию.", - "color_strip.color_cycle.colors": "Цвета:", - "color_strip.color_cycle.colors.hint": "Список цветов для плавной циклической смены. Минимум 2 цвета. По умолчанию — полный радужный спектр.", - "color_strip.color_cycle.add_color": "+ Добавить цвет", - "color_strip.color_cycle.speed": "Скорость:", - "color_strip.color_cycle.speed.hint": "Множитель скорости смены. 1.0 ≈ один полный цикл за 20 секунд; большие значения ускоряют смену.", - "color_strip.color_cycle.min_colors": "Смена цвета должна содержать не менее 2 цветов", - "color_strip.type.effect": "Эффект", - "color_strip.type.effect.desc": "Процедурные эффекты: огонь, плазма, аврора", - "color_strip.type.effect.hint": "Процедурные LED-эффекты (огонь, метеор, плазма, шум, аврора), генерируемые в реальном времени.", - "color_strip.type.composite": "Композит", - "color_strip.type.composite.desc": "Наложение и смешивание источников", - "color_strip.type.composite.hint": "Наложение нескольких источников цветовой ленты как слоёв с режимами смешивания и прозрачностью.", - "color_strip.type.mapped": "Маппинг", - "color_strip.type.mapped.desc": "Назначение источников на зоны LED", - "color_strip.type.mapped.hint": "Назначает разные источники цветовой полосы на разные диапазоны LED (зоны). В отличие от композита, маппинг размещает источники рядом друг с другом.", - "color_strip.type.audio": "Аудиореактив", - "color_strip.type.audio.desc": "LED от аудиосигнала", - "color_strip.type.audio.hint": "Цвета LED управляются аудиосигналом в реальном времени — системный звук или микрофон.", - "color_strip.type.api_input": "API-ввод", - "color_strip.type.api_input.desc": "Приём цветов от внешних приложений", - "color_strip.type.api_input.hint": "Принимает массивы цветов LED от внешних клиентов через REST POST или WebSocket. Используйте для интеграции с собственным ПО, домашней автоматизацией или любой системой, способной отправлять HTTP-запросы.", - "color_strip.api_input.fallback_color": "Цвет по умолчанию:", - "color_strip.api_input.fallback_color.hint": "Цвет для отображения, когда данные не получены в течение периода ожидания. LED покажут этот цвет при запуске и после потери соединения.", - "color_strip.api_input.timeout": "Тайм-аут (секунды):", - "color_strip.api_input.timeout.hint": "Время ожидания новых данных о цветах перед возвратом к цвету по умолчанию. Установите 0, чтобы не использовать тайм-аут.", - "color_strip.api_input.endpoints": "Эндпоинты для отправки:", - "color_strip.api_input.endpoints.hint": "Используйте эти URL для отправки данных о цветах LED из вашего внешнего приложения. REST принимает JSON, WebSocket принимает как JSON, так и бинарные кадры.", - "color_strip.api_input.save_first": "Сначала сохраните источник, чтобы увидеть URL эндпоинтов.", - "color_strip.api_input.interpolation": "Интерполяция LED:", - "color_strip.api_input.interpolation.hint": "Как масштабировать входящие данные LED, когда их количество отличается от количества LED на устройстве. Линейная — плавное смешивание, Ближайший — чёткие границы, Нет — обрезка или дополнение нулями.", - "color_strip.api_input.interpolation.linear": "Линейная", - "color_strip.api_input.interpolation.linear.desc": "Плавное смешивание между LED", - "color_strip.api_input.interpolation.nearest": "Ближайший", - "color_strip.api_input.interpolation.nearest.desc": "Чёткие границы, без смешивания", - "color_strip.api_input.interpolation.none": "Нет", - "color_strip.api_input.interpolation.none.desc": "Обрезка или дополнение нулями", - "color_strip.type.notification": "Уведомления", - "color_strip.type.notification.desc": "Разовый эффект по вебхуку", - "color_strip.type.notification.hint": "Вспышка, пульс или волна при срабатывании через вебхук. Предназначен для использования как слой в композитном источнике.", - "color_strip.notification.os_listener": "Слушать системные уведомления:", - "color_strip.notification.os_listener.hint": "Когда включено, источник автоматически срабатывает при появлении уведомления рабочего стола (Windows toast / Linux D-Bus). Требует разрешения на доступ к уведомлениям.", - "color_strip.notification.effect": "Эффект:", - "color_strip.notification.effect.hint": "Визуальный эффект при уведомлении. Вспышка — линейное затухание, Пульс — плавная волна, Волна — заполнение и затухание.", - "color_strip.notification.effect.flash": "Вспышка", - "color_strip.notification.effect.flash.desc": "Мгновенное включение, линейное затухание", - "color_strip.notification.effect.pulse": "Пульс", - "color_strip.notification.effect.pulse.desc": "Плавное свечение колоколом", - "color_strip.notification.effect.sweep": "Волна", - "color_strip.notification.effect.sweep.desc": "Заполняет слева направо, затем гаснет", - "color_strip.notification.duration": "Длительность (мс):", - "color_strip.notification.duration.hint": "Как долго длится эффект уведомления в миллисекундах.", - "color_strip.notification.default_color": "Цвет по умолчанию:", - "color_strip.notification.default_color.hint": "Цвет, когда для приложения нет специфического назначения цвета.", - "color_strip.notification.filter_mode": "Фильтр приложений:", - "color_strip.notification.filter_mode.hint": "Фильтр уведомлений по имени приложения. Выкл = все, Белый список = только указанные, Чёрный список = все кроме указанных.", - "color_strip.notification.filter_mode.off": "Выкл", - "color_strip.notification.filter_mode.whitelist": "Белый список", - "color_strip.notification.filter_mode.blacklist": "Чёрный список", - "color_strip.notification.filter_mode.off.desc": "Принимать все уведомления", - "color_strip.notification.filter_mode.whitelist.desc": "Только указанные приложения", - "color_strip.notification.filter_mode.blacklist.desc": "Все кроме указанных приложений", - "color_strip.notification.filter_list": "Список приложений:", - "color_strip.notification.filter_list.hint": "Одно имя приложения на строку. Используйте «Обзор» для выбора из запущенных процессов.", - "color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram", - "color_strip.notification.app_colors": "Цвета приложений", - "color_strip.notification.app_colors.label": "Назначения цветов:", - "color_strip.notification.app_colors.hint": "Индивидуальные цвета для приложений. Каждая строка связывает имя приложения с цветом уведомления.", - "color_strip.notification.app_colors.add": "+ Добавить", - "color_strip.notification.app_overrides": "Настройки приложений", - "color_strip.notification.app_overrides.label": "Переопределения:", - "color_strip.notification.app_overrides.hint": "Индивидуальные настройки цвета и звука для каждого приложения.", - "color_strip.notification.app_overrides.add": "+ Добавить", - "color_strip.notification.app_overrides.app_placeholder": "Имя приложения", - "color_strip.notification.sound": "Звук", - "color_strip.notification.sound.asset": "Звуковой ассет:", - "color_strip.notification.sound.asset.hint": "Выберите звуковой ассет для воспроизведения при уведомлении. Оставьте пустым для тишины.", - "color_strip.notification.sound.none": "Нет (без звука)", - "color_strip.notification.sound.search": "Поиск звуков…", - "color_strip.notification.sound.volume": "Громкость:", - "color_strip.notification.sound.volume.hint": "Общая громкость звуков уведомлений (0–100%).", - "color_strip.notification.sound.app_sounds": "Звуки приложений:", - "color_strip.notification.sound.app_sounds.hint": "Переопределение звука и громкости для конкретных приложений. Пустой звук = отключить для этого приложения.", - "color_strip.notification.sound.app_sounds.add": "+ Добавить", - "color_strip.notification.sound.app_name_placeholder": "Имя приложения", - "color_strip.notification.endpoint": "Вебхук:", - "color_strip.notification.endpoint.hint": "URL для запуска уведомлений из внешних систем. POST с JSON телом: {\"app\": \"AppName\", \"color\": \"#FF0000\"}.", - "color_strip.notification.save_first": "Сначала сохраните источник, чтобы увидеть URL вебхука.", - "color_strip.notification.app_count": "прилож.", - "color_strip.notification.test": "Тестовое уведомление", - "color_strip.notification.test.ok": "Уведомление отправлено", - "color_strip.notification.test.no_streams": "Нет запущенных потоков для этого источника", - "color_strip.notification.test.error": "Не удалось отправить уведомление", - "color_strip.notification.history.title": "История уведомлений", - "color_strip.notification.history.hint": "Последние ОС-уведомления, захваченные слушателем (новейшие сверху). До 50 записей.", - "color_strip.notification.history.empty": "Уведомления ещё не захвачены", - "color_strip.notification.history.unavailable": "Слушатель уведомлений ОС недоступен на этой платформе", - "color_strip.notification.history.error": "Не удалось загрузить историю уведомлений", - "color_strip.notification.history.refresh": "Обновить", - "color_strip.notification.history.unknown_app": "Неизвестное приложение", - "color_strip.notification.history.fired": "Потоков запущено", - "color_strip.notification.history.filtered": "Потоков отфильтровано", - "color_strip.test.title": "Предпросмотр", - "color_strip.test.connecting": "Подключение...", - "color_strip.test.error": "Не удалось подключиться к потоку предпросмотра", - "color_strip.test.led_count": "Кол-во LED:", - "color_strip.test.fps": "FPS:", - "color_strip.test.receive_fps": "Частота приёма", - "color_strip.test.apply": "Применить", - "color_strip.test.composite": "Композит", - "color_strip.preview.title": "Предпросмотр", - "color_strip.preview.not_connected": "Не подключено", - "color_strip.preview.connecting": "Подключение...", - "color_strip.preview.connected": "Подключено", - "color_strip.preview.unsupported": "Предпросмотр недоступен для этого типа источника", - "color_strip.type.daylight": "Дневной цикл", - "color_strip.type.daylight.desc": "Имитация естественного дневного света за 24 часа", - "color_strip.type.daylight.hint": "Имитирует цветовую температуру солнца в течение суток — от тёплого рассвета до прохладного дневного света, заката и ночи.", - "color_strip.daylight.speed": "Скорость:", - "color_strip.daylight.speed.hint": "Множитель скорости цикла. 1.0 = полный цикл день/ночь за ~4 минуты.", - "color_strip.daylight.use_real_time": "Реальное время:", - "color_strip.daylight.use_real_time.hint": "Если включено, цвет LED соответствует реальному времени суток. Настройка скорости игнорируется.", - "color_strip.daylight.real_time": "Реальное время", - "color_strip.daylight.latitude": "Широта:", - "color_strip.daylight.latitude.hint": "Географическая широта (-90 до 90). Влияет на время восхода/заката в режиме реального времени.", - "color_strip.type.candlelight": "Свечи", - "color_strip.type.candlelight.desc": "Реалистичная имитация мерцания свечей", - "color_strip.type.candlelight.hint": "Реалистичное мерцание свечей с тёплыми тонами и органическими паттернами.", - "color_strip.type.weather": "Погода", - "color_strip.type.weather.desc": "Погодно-реактивные амбиентные цвета", - "color_strip.type.weather.hint": "Отображает текущие погодные условия в амбиентные цвета LED. Требуется сущность Источник погоды.", - "color_strip.weather.source": "Источник погоды:", - "color_strip.weather.source.hint": "Источник метеоданных. Сначала создайте его во вкладке Погода.", - "color_strip.weather.speed": "Скорость анимации:", - "color_strip.weather.speed.hint": "Скорость дрейфа цветов. Выше = быстрее.", - "color_strip.weather.temperature_influence": "Влияние температуры:", - "color_strip.weather.temperature_influence.hint": "Насколько температура смещает палитру в тёплую/холодную сторону. 0 = чистые цвета условий, 1 = сильное смещение.", - "color_strip.weather.error.no_source": "Выберите источник погоды", - "color_strip.candlelight.color": "Базовый цвет:", - "color_strip.candlelight.color.hint": "Тёплый базовый цвет пламени свечи. По умолчанию — натуральный тёплый янтарь.", - "color_strip.candlelight.intensity": "Интенсивность мерцания:", - "color_strip.candlelight.intensity.hint": "Сила мерцания свечей. Низкие значения — мягкое свечение, высокие — свеча на ветру.", - "color_strip.candlelight.num_candles_label": "Количество свечей:", - "color_strip.candlelight.num_candles": "свечей", - "color_strip.candlelight.num_candles.hint": "Сколько независимых источников свечей вдоль ленты. Каждый мерцает по-своему.", - "color_strip.candlelight.speed": "Скорость мерцания:", - "color_strip.candlelight.speed.hint": "Скорость анимации мерцания. Большие значения — более быстрое, беспокойное пламя.", - "color_strip.type.processed": "Обработанный", - "color_strip.type.processed.desc": "Применить шаблон обработки к другому источнику", - "color_strip.type.processed.hint": "Оборачивает существующий источник цветовой полосы и пропускает его вывод через цепочку фильтров.", - "color_strip.processed.input": "Источник:", - "color_strip.processed.input.hint": "Источник цветовой полосы, вывод которого будет обработан", - "color_strip.processed.template": "Шаблон обработки:", - "color_strip.processed.template.hint": "Цепочка фильтров для применения к выводу входного источника", - "color_strip.processed.error.no_input": "Выберите входной источник", - "color_strip.composite.layers": "Слои:", - "color_strip.composite.layers.hint": "Наложение нескольких источников. Первый слой — нижний, последний — верхний. Каждый слой может иметь свой режим смешивания и прозрачность.", - "color_strip.composite.add_layer": "+ Добавить слой", - "color_strip.composite.source": "Источник", - "color_strip.composite.blend_mode": "Смешивание", - "color_strip.composite.blend_mode.normal": "Обычное", - "color_strip.composite.blend_mode.normal.desc": "Стандартное альфа-смешивание", - "color_strip.composite.blend_mode.add": "Сложение", - "color_strip.composite.blend_mode.add.desc": "Осветляет, складывая цвета", - "color_strip.composite.blend_mode.multiply": "Умножение", - "color_strip.composite.blend_mode.multiply.desc": "Затемняет, умножая цвета", - "color_strip.composite.blend_mode.screen": "Экран", - "color_strip.composite.blend_mode.screen.desc": "Осветляет, обратное умножение", - "color_strip.composite.blend_mode.override": "Замена", - "color_strip.composite.blend_mode.overlay": "Наложение", - "color_strip.composite.blend_mode.overlay.desc": "Умножение тёмных, осветление светлых", - "color_strip.composite.blend_mode.soft_light": "Мягкий свет", - "color_strip.composite.blend_mode.soft_light.desc": "Мягкая коррекция контраста", - "color_strip.composite.blend_mode.hard_light": "Жёсткий свет", - "color_strip.composite.blend_mode.hard_light.desc": "Сильный контраст, яркие цвета", - "color_strip.composite.blend_mode.difference": "Разница", - "color_strip.composite.blend_mode.difference.desc": "Абсолютная разница цветов", - "color_strip.composite.blend_mode.exclusion": "Исключение", - "color_strip.composite.blend_mode.exclusion.desc": "Как разница, но мягче", - "color_strip.composite.blend_mode.override.desc": "Чёрный = прозрачный, яркий = непрозрачный", - "color_strip.composite.opacity": "Непрозрачность", - "color_strip.composite.brightness": "Яркость", - "color_strip.composite.brightness.none": "Нет (полная яркость)", - "color_strip.composite.processing": "Обработка", - "color_strip.composite.enabled": "Включён", - "color_strip.composite.error.min_layers": "Необходим хотя бы 1 слой", - "color_strip.composite.error.no_source": "Для каждого слоя должен быть выбран источник", - "color_strip.composite.layers_count": "слоёв", - "color_strip.composite.range": "Диапазон LED", - "color_strip.composite.range_start": "Начало", - "color_strip.composite.range_end": "Конец", - "color_strip.composite.reverse": "Реверс", - "color_strip.mapped.zones": "Зоны:", - "color_strip.mapped.zones.hint": "Каждая зона привязывает источник цветовой полосы к определённому диапазону LED. Зоны размещаются рядом — промежутки остаются чёрными.", - "color_strip.mapped.add_zone": "+ Добавить зону", - "color_strip.mapped.zone_source": "Источник", - "color_strip.mapped.zone_start": "Начало LED", - "color_strip.mapped.zone_end": "Конец LED", - "color_strip.mapped.zone_reverse": "Реверс", - "color_strip.mapped.zones_count": "зон", - "color_strip.mapped.select_source": "Поиск источников...", - "color_strip.mapped.error.no_source": "Для каждой зоны должен быть выбран источник", - "color_strip.audio.visualization": "Визуализация:", - "color_strip.audio.visualization.hint": "Способ отображения аудиоданных на LED.", - "color_strip.audio.viz.spectrum": "Анализатор спектра", - "color_strip.audio.viz.spectrum.desc": "Частотные полосы по ленте", - "color_strip.audio.viz.beat_pulse": "Пульс бита", - "color_strip.audio.viz.beat_pulse.desc": "Все LED пульсируют в такт", - "color_strip.audio.viz.vu_meter": "VU-метр", - "color_strip.audio.viz.vu_meter.desc": "Уровень громкости заполняет ленту", - "color_strip.audio.source": "Аудиоисточник:", - "color_strip.audio.source.hint": "Аудиоисточник для визуализации. Может быть многоканальным (устройство) или моно (один канал). Создавайте и управляйте аудиоисточниками на вкладке Источники.", - "color_strip.audio.sensitivity": "Чувствительность:", - "color_strip.audio.sensitivity.hint": "Множитель усиления аудиосигнала. Более высокие значения делают LED чувствительнее к тихим звукам.", - "color_strip.audio.smoothing": "Сглаживание:", - "color_strip.audio.smoothing.hint": "Временное сглаживание между кадрами. Более высокие значения дают плавную, но медленнее реагирующую визуализацию.", - "color_strip.audio.palette": "Палитра:", - "color_strip.audio.palette.hint": "Цветовая палитра для полос спектра или пульсации бита.", - "color_strip.audio.color": "Базовый цвет:", - "color_strip.audio.color.hint": "Цвет низкого уровня для полосы VU-метра.", - "color_strip.audio.color_peak": "Пиковый цвет:", - "color_strip.audio.color_peak.hint": "Цвет высокого уровня в верхней части полосы VU-метра.", - "color_strip.audio.mirror": "Зеркало:", - "color_strip.audio.mirror.hint": "Зеркалирование спектра от центра к краям: басы в середине, высокие частоты по краям.", - "color_strip.effect.type": "Тип эффекта:", - "color_strip.effect.type.hint": "Выберите процедурный алгоритм.", - "color_strip.effect.fire": "Огонь", - "color_strip.effect.fire.desc": "Клеточный автомат, имитирующий поднимающееся пламя с диффузией тепла", - "color_strip.effect.meteor": "Метеор", - "color_strip.effect.meteor.desc": "Яркая точка движется по ленте с экспоненциально затухающим хвостом", - "color_strip.effect.plasma": "Плазма", - "color_strip.effect.plasma.desc": "Наложение синусоидальных волн с палитрой — классический демо-эффект", - "color_strip.effect.noise": "Шум", - "color_strip.effect.noise.desc": "Прокручиваемый фрактальный шум, отображённый на палитру", - "color_strip.effect.aurora": "Аврора", - "color_strip.effect.aurora.desc": "Наложенные шумовые полосы, дрейфующие и смешивающиеся — в стиле северного сияния", - "color_strip.effect.speed": "Скорость:", - "color_strip.effect.speed.hint": "Множитель скорости анимации эффекта (0.1 = очень медленно, 10.0 = очень быстро).", - "color_strip.effect.palette": "Палитра:", - "color_strip.effect.palette.hint": "Цветовая палитра для отображения значений эффекта в RGB-цвета.", - "color_strip.effect.color": "Цвет метеора:", - "color_strip.effect.color.hint": "Цвет головной точки метеора.", - "color_strip.effect.intensity": "Интенсивность:", - "color_strip.effect.intensity.hint": "Интенсивность эффекта — частота искр (огонь), затухание хвоста (метеор) или диапазон яркости (аврора).", - "color_strip.effect.scale": "Масштаб:", - "color_strip.effect.scale.hint": "Пространственный масштаб — частота волн (плазма), уровень масштабирования (шум) или ширина полос (аврора).", - "color_strip.effect.mirror": "Отражение:", - "color_strip.effect.mirror.hint": "Режим отскока — метеор меняет направление у краёв ленты вместо переноса.", - "color_strip.palette.fire": "Огонь", - "color_strip.palette.ocean": "Океан", - "color_strip.palette.lava": "Лава", - "color_strip.palette.forest": "Лес", - "color_strip.palette.rainbow": "Радуга", - "color_strip.palette.aurora": "Аврора", - "color_strip.palette.sunset": "Закат", - "color_strip.palette.ice": "Лёд", - "audio_source.title": "Аудиоисточники", - "audio_source.group.multichannel": "Многоканальные", - "audio_source.group.mono": "Моно", - "audio_source.group.band_extract": "Полосовой фильтр", - "audio_source.add": "Добавить аудиоисточник", - "audio_source.add.multichannel": "Добавить многоканальный", - "audio_source.add.mono": "Добавить моно", - "audio_source.add.band_extract": "Добавить полосовой фильтр", - "audio_source.edit": "Редактировать аудиоисточник", - "audio_source.edit.multichannel": "Редактировать многоканальный", - "audio_source.edit.mono": "Редактировать моно", - "audio_source.edit.band_extract": "Редактировать полосовой фильтр", - "audio_source.name": "Название:", - "audio_source.name.placeholder": "Системный звук", - "audio_source.name.hint": "Описательное имя для этого аудиоисточника", - "audio_source.type": "Тип:", - "audio_source.type.hint": "Многоканальный захватывает все каналы с аудиоустройства. Моно извлекает один канал из многоканального источника.", - "audio_source.type.multichannel": "Многоканальный", - "audio_source.type.mono": "Моно", - "audio_source.device": "Аудиоустройство:", - "audio_source.device.hint": "Источник аудиосигнала. Устройства обратной петли захватывают системный звук; устройства ввода — микрофон или линейный вход.", - "audio_source.refresh_devices": "Обновить устройства", - "audio_source.parent": "Родительский источник:", - "audio_source.parent.hint": "Многоканальный источник для извлечения канала", - "audio_source.channel": "Канал:", - "audio_source.channel.hint": "Какой аудиоканал извлечь из многоканального источника", - "audio_source.channel.mono": "Моно (Л+П микс)", - "audio_source.channel.left": "Левый", - "audio_source.channel.right": "Правый", - "audio_source.description": "Описание (необязательно):", - "audio_source.description.placeholder": "Опишите этот аудиоисточник...", - "audio_source.description.hint": "Необязательные заметки об этом аудиоисточнике", - "audio_source.created": "Аудиоисточник создан", - "audio_source.updated": "Аудиоисточник обновлён", - "audio_source.deleted": "Аудиоисточник удалён", - "audio_source.delete.confirm": "Удалить этот аудиоисточник?", - "audio_source.error.name_required": "Введите название", - "audio_source.audio_template": "Аудиошаблон:", - "audio_source.audio_template.hint": "Шаблон аудиозахвата определяет, какой движок и настройки использовать для этого устройства", - "audio_source.band_parent": "Родительский аудиоисточник:", - "audio_source.band_parent.hint": "Аудиоисточник для извлечения частотной полосы", - "audio_source.band": "Частотная полоса:", - "audio_source.band.hint": "Выберите предустановку частотной полосы или произвольный диапазон", - "audio_source.band.bass": "Басы (20–250 Гц)", - "audio_source.band.mid": "Средние (250–4000 Гц)", - "audio_source.band.treble": "Высокие (4000–20000 Гц)", - "audio_source.band.custom": "Произвольный диапазон", - "audio_source.freq_low": "Нижняя частота (Гц):", - "audio_source.freq_high": "Верхняя частота (Гц):", - "audio_source.freq_range": "Частотный диапазон", - "audio_source.test": "Тест", - "audio_source.test.title": "Тест аудиоисточника", - "audio_source.test.rms": "RMS", - "audio_source.test.peak": "Пик", - "audio_source.test.beat": "Бит", - "audio_source.test.connecting": "Подключение...", - "audio_source.test.error": "Ошибка теста аудио", - "audio_template.test": "Тест", - "audio_template.test.title": "Тест аудиошаблона", - "audio_template.test.device": "Аудиоустройство:", - "audio_template.test.device.hint": "Выберите устройство для захвата звука во время теста", - "audio_template.test.run": "Запуск", - "audio_template.title": "Аудиошаблоны", - "audio_template.add": "Добавить аудиошаблон", - "audio_template.edit": "Редактировать аудиошаблон", - "audio_template.name": "Название шаблона:", - "audio_template.name.placeholder": "Мой аудиошаблон", - "audio_template.description.label": "Описание (необязательно):", - "audio_template.description.placeholder": "Опишите этот шаблон...", - "audio_template.engine": "Аудиодвижок:", - "audio_template.engine.hint": "Выберите движок аудиозахвата. WASAPI — только Windows с поддержкой loopback. Sounddevice — кроссплатформенный.", - "audio_template.engine.unavailable": "Недоступен", - "audio_template.engine.unavailable.hint": "Этот движок недоступен в вашей системе", - "audio_template.config": "Конфигурация", - "audio_template.config.show": "Показать конфигурацию", - "audio_template.created": "Аудиошаблон создан", - "audio_template.updated": "Аудиошаблон обновлён", - "audio_template.deleted": "Аудиошаблон удалён", - "audio_template.delete.confirm": "Удалить этот аудиошаблон?", - "audio_template.error.load": "Не удалось загрузить аудиошаблоны", - "audio_template.error.engines": "Не удалось загрузить аудиодвижки", - "audio_template.error.required": "Пожалуйста, заполните все обязательные поля", - "audio_template.error.delete": "Не удалось удалить аудиошаблон", - "streams.group.value": "Источники значений", - "streams.group.sync": "Часы синхронизации", - "tree.group.picture": "Источники изображений", - "tree.group.capture": "Захват экрана", - "tree.group.static": "Статичные", - "tree.group.processing": "Обработанные", - "tree.group.strip": "Цветовые полосы", - "tree.group.audio": "Аудио", - "tree.group.utility": "Утилиты", - "tree.leaf.sources": "Источники", - "tree.leaf.engine_templates": "Шаблоны движка", - "tree.leaf.images": "Изображения", - "tree.leaf.video": "Видео", - "tree.leaf.filter_templates": "Шаблоны фильтров", - "tree.leaf.processing_templates": "Шаблоны обработки", - "tree.leaf.templates": "Шаблоны", - "value_source.group.title": "Источники значений", - "value_source.select_type": "Выберите тип источника значений", - "value_source.add": "Добавить источник значений", - "value_source.edit": "Редактировать источник значений", - "value_source.name": "Название:", - "value_source.name.placeholder": "Пульс яркости", - "value_source.name.hint": "Описательное имя для этого источника значений", - "value_source.type": "Тип:", - "value_source.type.hint": "Статический выдаёт постоянное значение. Анимированный циклически меняет форму волны. Аудио реагирует на звук. Адаптивные типы автоматически подстраивают яркость по времени суток или содержимому сцены.", - "value_source.type.static": "Статический", - "value_source.type.static.desc": "Постоянное выходное значение", - "value_source.type.animated": "Анимированный", - "value_source.type.animated.desc": "Циклическая смена по форме волны", - "value_source.type.audio": "Аудио", - "value_source.type.audio.desc": "Реагирует на звуковой сигнал", - "value_source.type.adaptive_time": "Адаптивный (Время)", - "value_source.type.adaptive_time.desc": "Подстройка по времени суток", - "value_source.type.adaptive_scene": "Адаптивный (Сцена)", - "value_source.type.adaptive_scene.desc": "Подстройка по содержимому сцены", - "value_source.type.daylight": "Дневной цикл", - "value_source.type.daylight.desc": "Яркость следует за циклом дня/ночи", - "value_source.daylight.speed": "Скорость:", - "value_source.daylight.speed.hint": "Множитель скорости цикла. 1.0 = полный цикл день/ночь за ~4 минуты.", - "value_source.daylight.use_real_time": "Реальное время:", - "value_source.daylight.use_real_time.hint": "Яркость следует за реальным временем суток. Скорость игнорируется.", - "value_source.daylight.enable_real_time": "Следовать за часами", - "value_source.daylight.latitude": "Широта:", - "value_source.daylight.latitude.hint": "Географическая широта (-90 до 90). Влияет на время восхода/заката в режиме реального времени.", - "value_source.daylight.real_time": "Реальное время", - "value_source.daylight.speed_label": "Скорость", - "value_source.value": "Значение:", - "value_source.value.hint": "Постоянное выходное значение (0.0 = выкл, 1.0 = полная яркость)", - "value_source.waveform": "Форма волны:", - "value_source.waveform.hint": "Форма цикла анимации яркости", - "value_source.waveform.sine": "Синус", - "value_source.waveform.triangle": "Треугольник", - "value_source.waveform.square": "Прямоугольник", - "value_source.waveform.sawtooth": "Пила", - "value_source.speed": "Скорость (цикл/мин):", - "value_source.speed.hint": "Циклов в минуту — как быстро повторяется волна (1 = очень медленно, 120 = очень быстро)", - "value_source.min_value": "Мин. значение:", - "value_source.min_value.hint": "Минимальный выход цикла волны", - "value_source.max_value": "Макс. значение:", - "value_source.max_value.hint": "Максимальный выход цикла волны", - "value_source.audio_source": "Аудиоисточник:", - "value_source.audio_source.hint": "Аудиоисточник для считывания уровня звука (многоканальный или моно)", - "value_source.mode": "Режим:", - "value_source.mode.hint": "RMS измеряет среднюю громкость. Пик отслеживает самые громкие моменты. Бит реагирует на ритм.", - "value_source.mode.rms": "RMS (Громкость)", - "value_source.mode.peak": "Пик", - "value_source.mode.beat": "Бит", - "value_source.mode.rms.desc": "Средний уровень громкости", - "value_source.mode.peak.desc": "Отслеживание пиковых моментов", - "value_source.mode.beat.desc": "Детекция ритмических ударов", - "value_source.auto_gain": "Авто-усиление:", - "value_source.auto_gain.hint": "Автоматически нормализует уровни звука, чтобы выходное значение использовало полный диапазон независимо от громкости входного сигнала", - "value_source.auto_gain.enable": "Включить авто-усиление", - "value_source.sensitivity": "Чувствительность:", - "value_source.sensitivity.hint": "Множитель усиления аудиосигнала (выше = более реактивный)", - "value_source.scene_sensitivity.hint": "Множитель усиления сигнала яркости (выше = более чувствительный к изменениям яркости)", - "value_source.smoothing": "Сглаживание:", - "value_source.smoothing.hint": "Временное сглаживание (0 = мгновенный отклик, 1 = очень плавный/медленный)", - "value_source.audio_min_value": "Мин. значение:", - "value_source.audio_min_value.hint": "Выход при тишине (напр. 0.3 = минимум 30% яркости)", - "value_source.audio_max_value": "Макс. значение:", - "value_source.audio_max_value.hint": "Выход при максимальном уровне звука", - "value_source.schedule": "Расписание:", - "value_source.schedule.hint": "Определите минимум 2 временные точки. Яркость линейно интерполируется между ними, с переходом через полночь.", - "value_source.schedule.add": "+ Добавить точку", - "value_source.schedule.points": "точек", - "value_source.picture_source": "Источник изображения:", - "value_source.picture_source.hint": "Источник изображения, кадры которого будут анализироваться на среднюю яркость.", - "value_source.scene_behavior": "Поведение:", - "value_source.scene_behavior.hint": "Дополнение: тёмная сцена = высокая яркость (для фоновой подсветки). Совпадение: яркая сцена = высокая яркость.", - "value_source.scene_behavior.complement": "Дополнение (тёмный → ярко)", - "value_source.scene_behavior.match": "Совпадение (яркий → ярко)", - "value_source.adaptive_min_value": "Мин. значение:", - "value_source.adaptive_min_value.hint": "Минимальная выходная яркость", - "value_source.adaptive_max_value": "Макс. значение:", - "value_source.adaptive_max_value.hint": "Максимальная выходная яркость", - "value_source.error.schedule_min": "Расписание требует минимум 2 временные точки", - "value_source.description": "Описание (необязательно):", - "value_source.description.placeholder": "Опишите этот источник значений...", - "value_source.description.hint": "Необязательные заметки об этом источнике значений", - "value_source.created": "Источник значений создан", - "value_source.updated": "Источник значений обновлён", - "value_source.deleted": "Источник значений удалён", - "value_source.delete.confirm": "Удалить этот источник значений?", - "value_source.error.name_required": "Введите название", - "value_source.test": "Тест", - "value_source.test.title": "Тест источника значений", - "value_source.test.connecting": "Подключение...", - "value_source.test.error": "Не удалось подключиться", - "value_source.test.current": "Текущее", - "value_source.test.min": "Мин", - "value_source.test.max": "Макс", - "test.frames": "Кадры", - "test.fps": "Кадр/с", - "test.avg_capture": "Сред", - "targets.brightness_vs": "Источник яркости:", - "targets.brightness_vs.hint": "Необязательный источник значений для динамического управления яркостью каждый кадр (переопределяет яркость устройства)", - "targets.brightness_vs.none": "Нет (яркость устройства)", - "targets.min_brightness_threshold": "Мин. порог яркости:", - "targets.min_brightness_threshold.hint": "Если итоговая яркость (яркость пикселей × яркость устройства/источника) ниже этого значения, светодиоды полностью выключаются (0 = отключено)", - "targets.adaptive_fps": "Адаптивный FPS:", - "targets.adaptive_fps.hint": "Автоматически снижает частоту отправки, когда устройство перестаёт отвечать, и постепенно восстанавливает её при стабилизации. Рекомендуется для WiFi-устройств со слабым сигналом.", - "targets.protocol": "Протокол:", - "targets.protocol.hint": "DDP отправляет пиксели по быстрому UDP (рекомендуется). HTTP использует JSON API — медленнее, но надёжнее, ограничение ~500 LED.", - "targets.protocol.ddp": "DDP (UDP)", - "targets.protocol.ddp.desc": "Быстрые UDP-пакеты — рекомендуется", - "targets.protocol.http": "HTTP", - "targets.protocol.http.desc": "JSON API — медленнее, ≤500 LED", - "targets.protocol.serial": "Serial", - "search.open": "Поиск (Ctrl+K)", - "search.placeholder": "Поиск... (Ctrl+K)", - "search.loading": "Загрузка...", - "search.no_results": "Ничего не найдено", - "search.group.devices": "Устройства", - "search.group.targets": "LED-цели", - "search.group.kc_targets": "Цели Key Colors", - "search.group.css": "Источники цветных лент", - "search.group.automations": "Автоматизации", - "search.group.streams": "Потоки изображений", - "search.group.capture_templates": "Шаблоны захвата", - "search.group.pp_templates": "Шаблоны постобработки", - "search.group.pattern_templates": "Шаблоны паттернов", - "search.group.audio": "Аудиоисточники", - "search.group.value": "Источники значений", - "search.group.scenes": "Пресеты сцен", - "search.group.cspt": "Шаблоны обработки полос", - "search.group.sync_clocks": "Синхронные часы", - "search.group.actions": "Действия", - "search.action.start": "Запустить", - "search.action.stop": "Остановить", - "search.action.activate": "Активировать", - "search.action.enable": "Включить", - "search.action.disable": "Отключить", - "settings.backup.label": "Резервное копирование", - "settings.backup.hint": "Скачать всю конфигурацию (устройства, цели, потоки, шаблоны, автоматизации) в виде одного JSON-файла.", - "settings.backup.button": "Скачать резервную копию", - "settings.backup.success": "Резервная копия скачана", - "settings.backup.error": "Ошибка скачивания резервной копии", - "settings.restore.label": "Восстановление конфигурации", - "settings.restore.hint": "Загрузите ранее сохранённый файл резервной копии для замены всей конфигурации. Сервер перезапустится автоматически.", - "settings.restore.button": "Восстановить из копии", - "settings.restore.confirm": "Это заменит ВСЮ конфигурацию и перезапустит сервер. Вы уверены?", - "settings.restore.success": "Конфигурация восстановлена", - "settings.restore.error": "Ошибка восстановления", - "settings.restore.restarting": "Сервер перезапускается...", - "settings.restore.restart_timeout": "Сервер не отвечает. Обновите страницу вручную.", - "settings.restart_server": "Перезапустить сервер", - "settings.restart_confirm": "Перезапустить сервер? Активные цели будут остановлены.", - "settings.restarting": "Перезапуск сервера...", - "settings.button.close": "Закрыть", - "settings.log_level.label": "Уровень логирования", - "settings.log_level.hint": "Изменить подробность логов сервера в реальном времени. DEBUG — максимум деталей, CRITICAL — только критические ошибки.", - "settings.log_level.save": "Применить", - "settings.log_level.saved": "Уровень логирования изменён", - "settings.log_level.save_error": "Не удалось изменить уровень логирования", - "settings.log_level.desc.debug": "Подробный вывод для разработки", - "settings.log_level.desc.info": "Обычные сообщения", - "settings.log_level.desc.warning": "Возможные проблемы", - "settings.log_level.desc.error": "Только ошибки", - "settings.log_level.desc.critical": "Только критические ошибки", - "settings.auto_backup.label": "Авто-бэкап", - "settings.auto_backup.hint": "Автоматическое создание периодических резервных копий конфигурации. Старые копии удаляются при превышении максимального количества.", - "settings.auto_backup.enable": "Включить авто-бэкап", - "settings.auto_backup.interval_label": "Интервал", - "settings.auto_backup.max_label": "Макс. копий", - "settings.auto_backup.save": "Сохранить настройки", - "settings.auto_backup.saved": "Настройки авто-бэкапа сохранены", - "settings.auto_backup.save_error": "Не удалось сохранить настройки авто-бэкапа", - "settings.auto_backup.backup_now": "Создать бэкап", - "settings.auto_backup.backup_created": "Бэкап создан", - "settings.auto_backup.backup_error": "Ошибка создания бэкапа", - "settings.auto_backup.last_backup": "Последний бэкап", - "settings.auto_backup.never": "Никогда", - "settings.saved_backups.label": "Сохранённые копии", - "settings.saved_backups.hint": "Файлы авто-бэкапа на сервере. Скачайте для локального хранения или удалите для освобождения места.", - "settings.saved_backups.empty": "Нет сохранённых копий", - "settings.saved_backups.restore": "Восстановить", - "settings.saved_backups.download": "Скачать", - "settings.saved_backups.delete": "Удалить", - "settings.saved_backups.delete_confirm": "Удалить эту резервную копию?", - "settings.saved_backups.delete_error": "Не удалось удалить копию", - "settings.saved_backups.type.auto": "авто", - "settings.saved_backups.type.manual": "ручной", - "settings.mqtt.label": "MQTT", - "settings.mqtt.hint": "Настройте подключение к MQTT-брокеру для условий и триггеров автоматизации.", - "settings.mqtt.enabled": "Включить MQTT", - "settings.mqtt.host_label": "Хост брокера", - "settings.mqtt.port_label": "Порт", - "settings.mqtt.username_label": "Имя пользователя", - "settings.mqtt.password_label": "Пароль", - "settings.mqtt.password_set_hint": "Пароль задан — оставьте пустым, чтобы сохранить", - "settings.mqtt.client_id_label": "Идентификатор клиента", - "settings.mqtt.base_topic_label": "Базовый топик", - "settings.mqtt.save": "Сохранить настройки MQTT", - "settings.mqtt.saved": "Настройки MQTT сохранены", - "settings.mqtt.save_error": "Не удалось сохранить настройки MQTT", - "settings.mqtt.error_host_required": "Требуется указать хост брокера", - "settings.logs.label": "Журнал сервера", - "settings.logs.hint": "Просмотр журнала сервера в реальном времени. Используйте фильтр для отображения нужных уровней.", - "settings.logs.connect": "Подключить", - "settings.logs.disconnect": "Отключить", - "settings.logs.clear": "Очистить", - "settings.logs.error": "Ошибка подключения к журналу", - "settings.logs.filter.all": "Все уровни", - "settings.logs.filter.info": "Info+", - "settings.logs.filter.warning": "Warning+", - "settings.logs.filter.error": "Только ошибки", - "settings.logs.filter.all_desc": "Все сообщения лога", - "settings.logs.filter.info_desc": "Info, предупреждения и ошибки", - "settings.logs.filter.warning_desc": "Только предупреждения и ошибки", - "settings.logs.filter.error_desc": "Только ошибки", - "device.error.power_off_failed": "Не удалось выключить устройство", - "device.error.remove_failed": "Не удалось удалить устройство", - "device.error.settings_load_failed": "Не удалось загрузить настройки устройства", - "device.error.brightness": "Не удалось обновить яркость", - "device.error.required": "Пожалуйста, заполните все поля", - "device.error.update": "Не удалось обновить устройство", - "device.error.save": "Не удалось сохранить настройки", - "device.error.clone_failed": "Не удалось клонировать устройство", - "device_discovery.error.fill_all_fields": "Пожалуйста, заполните все поля", - "device_discovery.added": "Устройство успешно добавлено", - "device_discovery.error.add_failed": "Не удалось добавить устройство", - "calibration.error.load_failed": "Не удалось загрузить калибровку", - "calibration.error.css_load_failed": "Не удалось загрузить источник цветовой полосы", - "calibration.error.test_toggle_failed": "Не удалось переключить тестовый край", - "calibration.error.save_failed": "Не удалось сохранить калибровку", - "calibration.error.led_count_mismatch": "Общее количество LED должно совпадать с количеством LED устройства", - "calibration.error.led_count_exceeded": "Калиброванных LED больше, чем общее количество LED", - "calibration.mode.simple": "Простой", - "calibration.mode.advanced": "Расширенный", - "calibration.switch_to_advanced": "Расширенный режим", - "calibration.advanced.title": "Расширенная калибровка", - "calibration.advanced.switch_to_simple": "Простой режим", - "calibration.advanced.lines_title": "Линии", - "calibration.advanced.canvas_hint": "Перетаскивайте мониторы. Нажимайте на грани для выбора линий. Прокрутка — масштаб, перетаскивание пустого места — сдвиг.", - "calibration.advanced.reset_view": "Сбросить вид", - "calibration.advanced.line_properties": "Свойства линии", - "calibration.advanced.picture_source": "Источник:", - "calibration.advanced.picture_source.hint": "Источник изображения (монитор), с которого эта линия снимает данные", - "calibration.advanced.edge": "Грань:", - "calibration.advanced.edge.hint": "С какой грани экрана снимать пиксели", - "calibration.advanced.led_count": "Светодиоды:", - "calibration.advanced.led_count.hint": "Количество светодиодов на этой линии", - "calibration.advanced.span_start": "Начало:", - "calibration.advanced.span_start.hint": "Откуда начинается захват вдоль грани (0 = начало, 1 = конец). Позволяет покрыть только часть грани.", - "calibration.advanced.span_end": "Конец:", - "calibration.advanced.span_end.hint": "Где заканчивается захват вдоль грани (0 = начало, 1 = конец). Вместе с «Начало» определяет активный участок.", - "calibration.advanced.border_width": "Глубина (пкс):", - "calibration.advanced.border_width.hint": "Сколько пикселей вглубь от края захватывать. Большие значения берут больше внутренней части экрана.", - "calibration.advanced.reverse": "Реверс", - "calibration.advanced.no_lines_warning": "Добавьте хотя бы одну линию", - "dashboard.error.automation_toggle_failed": "Не удалось переключить автоматизацию", - "dashboard.error.start_failed": "Не удалось запустить обработку", - "dashboard.error.stop_failed": "Не удалось остановить обработку", - "dashboard.error.stop_all": "Не удалось остановить все цели", - "target.error.editor_open_failed": "Не удалось открыть редактор цели", - "target.error.start_failed": "Не удалось запустить цель", - "target.error.stop_failed": "Не удалось остановить цель", - "target.error.clone_failed": "Не удалось клонировать цель", - "target.error.delete_failed": "Не удалось удалить цель", - "targets.stop_all.button": "Остановить все", - "targets.stop_all.none_running": "Нет запущенных целей", - "targets.stop_all.stopped": "Остановлено целей: {count}", - "targets.stop_all.error": "Не удалось остановить цели", - "audio_source.error.load": "Не удалось загрузить аудиоисточник", - "audio_template.error.clone_failed": "Не удалось клонировать аудиошаблон", - "value_source.error.load": "Не удалось загрузить источник значений", - "color_strip.error.editor_open_failed": "Не удалось открыть редактор цветовой полосы", - "color_strip.error.clone_failed": "Не удалось клонировать источник цветовой полосы", - "color_strip.error.delete_failed": "Не удалось удалить источник цветовой полосы", - "pattern.error.editor_open_failed": "Не удалось открыть редактор шаблона узоров", - "pattern.error.clone_failed": "Не удалось клонировать шаблон узоров", - "pattern.error.delete_failed": "Не удалось удалить шаблон узоров", - "pattern.error.capture_bg_failed": "Не удалось захватить фон", - "stream.error.clone_picture_failed": "Не удалось клонировать источник изображения", - "stream.error.clone_capture_failed": "Не удалось клонировать шаблон захвата", - "stream.error.clone_pp_failed": "Не удалось клонировать шаблон постобработки", - "kc_target.error.editor_open_failed": "Не удалось открыть редактор ключевых цветов", - "kc_target.error.clone_failed": "Не удалось клонировать цель ключевых цветов", - "kc_target.error.delete_failed": "Не удалось удалить цель ключевых цветов", - "theme.switched.dark": "Переключено на тёмную тему", - "theme.switched.light": "Переключено на светлую тему", - "accent.color.updated": "Цвет акцента обновлён", - "search.footer": "↑↓ навигация · Enter выбор · Esc закрыть", - "sync_clock.group.title": "Часы синхронизации", - "sync_clock.add": "Добавить часы", - "sync_clock.edit": "Редактировать часы", - "sync_clock.name": "Название:", - "sync_clock.name.placeholder": "Основные часы анимации", - "sync_clock.name.hint": "Описательное название для этих часов синхронизации", - "sync_clock.speed": "Скорость:", - "sync_clock.speed.hint": "Множитель скорости анимации для всех привязанных источников. 1.0 = обычная, 2.0 = двойная, 0.5 = половинная.", - "sync_clock.description": "Описание (необязательно):", - "sync_clock.description.placeholder": "Необязательное описание", - "sync_clock.description.hint": "Необязательные заметки о назначении этих часов", - "sync_clock.status.running": "Работает", - "sync_clock.status.paused": "Приостановлено", - "sync_clock.action.pause": "Приостановить", - "sync_clock.action.resume": "Возобновить", - "sync_clock.action.reset": "Сбросить", - "sync_clock.error.name_required": "Название часов обязательно", - "sync_clock.error.load": "Не удалось загрузить часы синхронизации", - "sync_clock.created": "Часы синхронизации созданы", - "sync_clock.updated": "Часы синхронизации обновлены", - "sync_clock.deleted": "Часы синхронизации удалены", - "sync_clock.paused": "Часы приостановлены", - "sync_clock.resumed": "Часы возобновлены", - "sync_clock.reset_done": "Часы сброшены на ноль", - "sync_clock.delete.confirm": "Удалить эти часы синхронизации? Привязанные источники потеряют синхронизацию и будут работать на скорости по умолчанию.", - "sync_clock.elapsed": "Прошло времени", - "weather_source.group.title": "Источники погоды", - "weather_source.add": "Добавить источник погоды", - "weather_source.edit": "Редактировать источник погоды", - "weather_source.name": "Название:", - "weather_source.name.placeholder": "Моя погода", - "weather_source.name.hint": "Описательное название источника погоды", - "weather_source.provider": "Провайдер:", - "weather_source.provider.hint": "Провайдер метеоданных. Open-Meteo бесплатный и не требует API ключа.", - "weather_source.provider.open_meteo.desc": "Бесплатно, без API ключа", - "weather_source.location": "Местоположение:", - "weather_source.location.hint": "Географические координаты. Используйте автоопределение или введите вручную.", - "weather_source.latitude": "Шир:", - "weather_source.longitude": "Долг:", - "weather_source.use_my_location": "Определить", - "weather_source.update_interval": "Интервал обновления:", - "weather_source.update_interval.hint": "Частота запроса метеоданных. Меньше = более оперативные обновления.", - "weather_source.description": "Описание (необязательно):", - "weather_source.description.placeholder": "Необязательное описание", - "weather_source.test": "Тест", - "weather_source.error.name_required": "Название источника погоды обязательно", - "weather_source.error.load": "Не удалось загрузить источник погоды", - "weather_source.created": "Источник погоды создан", - "weather_source.updated": "Источник погоды обновлён", - "weather_source.deleted": "Источник погоды удалён", - "weather_source.delete.confirm": "Удалить этот источник погоды? Связанные источники цветовых лент потеряют данные о погоде.", - "weather_source.geo.success": "Местоположение определено", - "weather_source.geo.error": "Ошибка геолокации", - "weather_source.geo.not_supported": "Геолокация не поддерживается вашим браузером", - "streams.group.weather": "Погода", - "section.empty.weather_sources": "Нет источников погоды. Нажмите + для добавления.", - "color_strip.clock": "Часы синхронизации:", - "color_strip.clock.hint": "Привязка к часам для синхронизации анимации между источниками. Скорость управляется на часах.", - "graph.title": "Граф", - "graph.fit_all": "Показать все узлы", - "graph.zoom_in": "Приблизить", - "graph.zoom_out": "Отдалить", - "graph.search": "Поиск узлов", - "graph.search_placeholder": "Поиск сущностей...", - "graph.legend": "Легенда", - "graph.minimap": "Миникарта", - "graph.relayout": "Перестроить", - "graph.empty": "Ещё нет сущностей", - "graph.empty.hint": "Создайте устройства, источники и цели, чтобы увидеть их здесь.", - "graph.disconnect": "Отключить", - "graph.connection_updated": "Соединение обновлено", - "graph.connection_failed": "Не удалось обновить соединение", - "graph.connection_removed": "Соединение удалено", - "graph.disconnect_failed": "Не удалось отключить", - "graph.relayout_confirm": "Сбросить все ручные позиции узлов и перестроить граф?", - "graph.fullscreen": "Полноэкранный режим", - "graph.add_entity": "Добавить сущность", - "graph.color_picker": "Цвет узла", - "graph.filter": "Фильтр узлов", - "graph.filter_placeholder": "Фильтр по имени...", - "graph.filter_clear": "Очистить фильтр", - "graph.filter_running": "Запущен", - "graph.filter_stopped": "Остановлен", - "graph.filter_types": "Типы", - "graph.filter_group.capture": "Захват", - "graph.filter_group.strip": "Цвет. полосы", - "graph.filter_group.audio": "Аудио", - "graph.filter_group.targets": "Цели", - "graph.filter_group.other": "Другое", - "graph.bulk_delete_confirm": "Удалить {count} выбранных сущностей?", - "graph.nothing_to_undo": "Нечего отменять", - "graph.nothing_to_redo": "Нечего повторять", - "graph.help_title": "Горячие клавиши", - "graph.help.search": "Поиск", - "graph.help.filter": "Фильтр", - "graph.help.add": "Добавить сущность", - "graph.help.shortcuts": "Горячие клавиши", - "graph.help.delete": "Удалить / Отсоединить", - "graph.help.select_all": "Выбрать все", - "graph.help.undo": "Отменить", - "graph.help.redo": "Повторить", - "graph.help.fullscreen": "Полный экран", - "graph.help.deselect": "Снять выбор", - "graph.help.navigate": "Навигация по узлам", - "graph.help.click": "Клик", - "graph.help.click_desc": "Выбрать узел", - "graph.help.dblclick": "Двойной клик", - "graph.help.dblclick_desc": "Приблизить к узлу", - "graph.help.shift_click": "Shift+Клик", - "graph.help.shift_click_desc": "Множественный выбор", - "graph.help.shift_drag": "Shift+Перетащить", - "graph.help.shift_drag_desc": "Выбор рамкой", - "graph.help.drag_node": "Перетащить узел", - "graph.help.drag_node_desc": "Переместить", - "graph.help.drag_port": "Перетащить порт", - "graph.help.drag_port_desc": "Соединить сущности", - "graph.help.right_click": "ПКМ по связи", - "graph.help.right_click_desc": "Отсоединить связь", - "graph.tooltip.fps": "FPS", - "graph.tooltip.errors": "Ошибки", - "graph.tooltip.uptime": "Время работы", - "automation.enabled": "Автоматизация включена", - "automation.disabled": "Автоматизация выключена", - "scene_preset.activated": "Пресет активирован", - "scene_preset.used_by": "Используется в %d автоматизации(ях)", - "settings.api_keys.label": "API-ключи", - "settings.api_keys.hint": "API-ключи определяются в конфигурационном файле сервера (config.yaml). Отредактируйте файл и перезапустите сервер для применения изменений.", - "settings.api_keys.empty": "API-ключи не настроены", - "settings.api_keys.load_error": "Не удалось загрузить API-ключи", - "settings.partial.label": "Частичный экспорт / импорт", - "settings.partial.hint": "Экспортировать или импортировать один тип объектов. Импорт заменяет или объединяет данные и перезапускает сервер.", - "settings.partial.store.devices": "Устройства", - "settings.partial.store.output_targets": "LED-цели", - "settings.partial.store.color_strip_sources": "Цветные полосы", - "settings.partial.store.picture_sources": "Источники изображений", - "settings.partial.store.audio_sources": "Аудио-источники", - "settings.partial.store.audio_templates": "Аудио-шаблоны", - "settings.partial.store.capture_templates": "Шаблоны захвата", - "settings.partial.store.postprocessing_templates": "Шаблоны постобработки", - "settings.partial.store.color_strip_processing_templates": "Шаблоны обработки полос", - "settings.partial.store.pattern_templates": "Шаблоны паттернов", - "settings.partial.store.value_sources": "Источники значений", - "settings.partial.store.sync_clocks": "Синхронные часы", - "settings.partial.store.automations": "Автоматизации", - "settings.partial.store.scene_presets": "Пресеты сцен", - "settings.partial.export_button": "Экспорт", - "settings.partial.import_button": "Импорт из файла", - "settings.partial.merge_label": "Объединить (добавить/перезаписать, сохранить существующие)", - "settings.partial.export_success": "Экспорт выполнен", - "settings.partial.export_error": "Ошибка экспорта", - "settings.partial.import_success": "Импорт выполнен", - "settings.partial.import_error": "Ошибка импорта", - "settings.partial.import_confirm_replace": "Это ЗАМЕНИТ все данные {store} и перезапустит сервер. Продолжить?", - "settings.partial.import_confirm_merge": "Это ОБЪЕДИНИТ данные {store} и перезапустит сервер. Продолжить?", - "section.empty.devices": "Устройств пока нет. Нажмите + для добавления.", - "section.empty.targets": "LED-целей пока нет. Нажмите + для добавления.", - "section.empty.kc_targets": "Целей ключевых цветов пока нет. Нажмите + для добавления.", - "section.empty.pattern_templates": "Шаблонов паттернов пока нет. Нажмите + для добавления.", - "section.empty.picture_sources": "Источников пока нет. Нажмите + для добавления.", - "section.empty.capture_templates": "Шаблонов захвата пока нет. Нажмите + для добавления.", - "section.empty.pp_templates": "Шаблонов постобработки пока нет. Нажмите + для добавления.", - "section.empty.audio_sources": "Аудио-источников пока нет. Нажмите + для добавления.", - "section.empty.audio_templates": "Аудио-шаблонов пока нет. Нажмите + для добавления.", - "section.empty.color_strips": "Цветных полос пока нет. Нажмите + для добавления.", - "section.empty.value_sources": "Источников значений пока нет. Нажмите + для добавления.", - "section.empty.sync_clocks": "Синхронных часов пока нет. Нажмите + для добавления.", - "section.empty.cspt": "Шаблонов обработки полос пока нет. Нажмите + для добавления.", - "section.empty.automations": "Автоматизаций пока нет. Нажмите + для добавления.", - "section.empty.scenes": "Пресетов сцен пока нет. Нажмите + для добавления.", - "bulk.select": "Выбрать", - "bulk.cancel": "Отмена", - "bulk.selected_count.one": "{count} выбран", - "bulk.selected_count.few": "{count} выбрано", - "bulk.selected_count.many": "{count} выбрано", - "bulk.select_all": "Выбрать все", - "bulk.deselect_all": "Снять выбор", - "bulk.delete": "Удалить", - "bulk.start": "Запустить", - "bulk.stop": "Остановить", - "bulk.enable": "Включить", - "bulk.disable": "Выключить", - "bulk.confirm_delete.one": "Удалить {count} элемент?", - "bulk.confirm_delete.few": "Удалить {count} элемента?", - "bulk.confirm_delete.many": "Удалить {count} элементов?", - "appearance.style.label": "Стили оформления", - "appearance.style.hint": "Выберите визуальную тему — шрифт и цветовая палитра применяются вместе.", - "appearance.preset.default": "Стандарт", - "appearance.preset.midnight": "Полночь", - "appearance.preset.ember": "Угли", - "appearance.preset.arctic": "Арктика", - "appearance.preset.terminal": "Терминал", - "appearance.preset.neon": "Неон", - "appearance.preset.sakura": "Сакура", - "appearance.preset.ocean": "Океан", - "appearance.preset.copper": "Медь", - "appearance.preset.vapor": "Вейпорвейв", - "appearance.preset.monolith": "Монолит", - "appearance.preset.applied": "Стиль применён", - "appearance.bg.label": "Фоновые эффекты", - "appearance.bg.hint": "Добавьте фоновый слой за интерфейсом.", - "appearance.bg.none": "Нет", - "appearance.bg.noise": "Шумовое поле", - "appearance.bg.aurora": "Северное сияние", - "appearance.bg.plasma": "Плазма", - "appearance.bg.rain": "Цифровой дождь", - "appearance.bg.stars": "Звёздное поле", - "appearance.bg.warp": "Тоннель", - "appearance.bg.grid": "Точечная сетка", - "appearance.bg.mesh": "Градиент", - "appearance.bg.scanlines": "Развёртка", - "appearance.bg.applied": "Фоновый эффект применён", - - "settings.tab.updates": "Обновления", - "settings.tab.about": "О программе", - "update.status_label": "Статус обновления", - "update.current_version": "Текущая версия:", - "update.badge_tooltip": "Доступна новая версия — нажмите для подробностей", - "update.available": "Доступна версия {version}", - "update.up_to_date": "Установлена последняя версия", - "update.prerelease": "пре-релиз", - "update.view_release": "Подробнее", - "update.dismiss": "Скрыть", - "update.check_now": "Проверить обновления", - "update.check_error": "Ошибка проверки обновлений", - "update.last_check": "Последняя проверка", - "update.never": "никогда", - "update.release_notes": "Примечания к релизу", - "update.view_release_notes": "Открыть примечания к релизу", - "update.auto_check_label": "Автоматическая проверка", - "update.auto_check_hint": "Периодически проверять наличие новых версий в фоновом режиме.", - "update.enable": "Включить автопроверку", - "update.interval_label": "Интервал проверки", - "update.channel_label": "Канал", - "update.channel.stable": "Стабильный", - "update.channel.stable_desc": "Только стабильные релизы", - "update.channel.prerelease": "Пре-релиз", - "update.channel.prerelease_desc": "Включая альфа, бета и RC сборки", - "update.save_settings": "Сохранить настройки", - "update.settings_saved": "Настройки обновлений сохранены", - "update.settings_save_error": "Не удалось сохранить настройки обновлений", - "update.apply_now": "Обновить сейчас", - "update.apply_confirm": "Скачать и установить версию {version}? Сервер будет перезапущен автоматически.", - "update.apply_error": "Ошибка обновления", - "update.applying": "Применяется обновление…", - "update.downloading": "Загрузка…", - "update.install_type_label": "Тип установки:", - "update.install_type.installer": "Установщик Windows", - "update.install_type.portable": "Портативная", - "update.install_type.docker": "Docker", - "update.install_type.dev": "Разработка", - - "color_strip.notification.search_apps": "Поиск приложений…", - - "asset.group.title": "Ресурсы", - "asset.upload": "Загрузить ресурс", - "asset.edit": "Редактировать ресурс", - "asset.name": "Название:", - "asset.name.hint": "Отображаемое название ресурса.", - "asset.description": "Описание:", - "asset.description.hint": "Необязательное описание ресурса.", - "asset.file": "Файл:", - "asset.file.hint": "Выберите файл для загрузки (звук, изображение, видео или другое).", - "asset.drop_or_browse": "Перетащите файл сюда или нажмите для выбора", - "asset.uploaded": "Ресурс загружен", - "asset.updated": "Ресурс обновлён", - "asset.deleted": "Ресурс удалён", - "asset.confirm_delete": "Удалить этот ресурс?", - "asset.error.name_required": "Название обязательно", - "asset.error.no_file": "Выберите файл для загрузки", - "asset.error.delete_failed": "Не удалось удалить ресурс", - "asset.error.play_failed": "Не удалось воспроизвести звук", - "asset.error.download_failed": "Не удалось скачать ресурс", - "asset.play": "Воспроизвести", - "asset.download": "Скачать", - "asset.prebuilt": "Встроенный", - "asset.prebuilt_restored": "Восстановлено встроенных ресурсов: {count}", - "asset.prebuilt_none_to_restore": "Все встроенные ресурсы уже доступны", - "asset.restore_prebuilt": "Восстановить встроенные звуки", - "asset.type.sound": "Звук", - "asset.type.image": "Изображение", - "asset.type.video": "Видео", - "asset.type.other": "Другое", - "streams.group.assets": "Ресурсы", - "section.empty.assets": "Ресурсов пока нет. Нажмите +, чтобы загрузить.", - - "donation.message": "LedGrab — бесплатный проект с открытым кодом. Если он вам полезен, поддержите разработку.", - "donation.support": "Поддержать проект", - "donation.view_source": "Исходный код", - "donation.later": "Напомнить позже", - "donation.dismiss": "Больше не показывать", - "donation.about_title": "О LedGrab", - "donation.about_opensource": "LedGrab — программа с открытым исходным кодом, бесплатная для использования и модификации.", - "donation.about_donate": "Поддержать разработку", - "donation.about_license": "Лицензия MIT" -} \ No newline at end of file + "app.title": "LED Grab", + "app.version": "Версия:", + "app.api_docs": "Документация API", + "app.connection_lost": "Сервер недоступен", + "app.connection_retrying": "Попытка переподключения…", + "app.server_restarting": "Сервер перезапускается…", + "app.server_restarting_sub": "Пожалуйста, подождите, сервер скоро вернётся.", + "demo.badge": "ДЕМО", + "demo.banner": "Вы в демо-режиме — все устройства и данные виртуальные. Реальное оборудование не используется.", + "theme.toggle": "Переключить тему", + "bg.anim.toggle": "Анимированный фон", + "accent.title": "Цвет акцента", + "accent.custom": "Свой", + "accent.reset": "Сброс", + "locale.change": "Изменить язык", + "auth.login": "Войти", + "auth.logout": "Выйти", + "auth.authenticated": "● Авторизован", + "auth.title": "Вход в LED Grab", + "auth.message": "Пожалуйста, введите ваш API ключ для аутентификации и доступа к LED Grab.", + "auth.label": "API Ключ:", + "auth.placeholder": "Введите ваш API ключ...", + "auth.hint": "Ваш API ключ будет безопасно сохранен в локальном хранилище браузера.", + "auth.button.cancel": "Отмена", + "auth.button.login": "Войти", + "auth.error.required": "Пожалуйста, введите API ключ", + "auth.success": "Вход выполнен успешно!", + "auth.logout.confirm": "Вы уверены, что хотите выйти?", + "auth.logout.success": "Выход выполнен успешно", + "auth.please_login": "Пожалуйста, войдите для просмотра", + "auth.session_expired": "Ваша сессия истекла или API ключ недействителен. Пожалуйста, войдите снова.", + "auth.toggle_password": "Показать/скрыть пароль", + "auth.prompt_enter": "Enter your API key:", + "auth.prompt_update": "Current API key is set. Enter new key to update or leave blank to remove:", + "api_key.login": "Войти", + "displays.title": "Доступные Дисплеи", + "displays.layout": "Дисплеи", + "displays.information": "Информация о Дисплеях", + "displays.legend.primary": "Основной Дисплей", + "displays.legend.secondary": "Вторичный Дисплей", + "displays.badge.primary": "Основной", + "displays.badge.secondary": "Вторичный", + "displays.resolution": "Разрешение:", + "displays.refresh_rate": "Частота Обновления:", + "displays.position": "Позиция:", + "displays.index": "Индекс Дисплея:", + "displays.loading": "Загрузка дисплеев...", + "displays.none": "Нет доступных дисплеев", + "displays.failed": "Не удалось загрузить дисплеи", + "displays.picker.title": "Выберите Дисплей", + "displays.picker.title.device": "Выберите Устройство", + "displays.picker.select": "Выберите дисплей...", + "displays.picker.click_to_select": "Нажмите, чтобы выбрать этот дисплей", + "displays.picker.adb_connect": "Подключить ADB устройство", + "displays.picker.adb_connect.placeholder": "IP адрес (напр. 192.168.2.201)", + "displays.picker.adb_connect.button": "Подключить", + "displays.picker.adb_connect.success": "Устройство подключено", + "displays.picker.adb_connect.error": "Не удалось подключить устройство", + "displays.picker.adb_disconnect": "Отключить", + "displays.picker.no_android": "Android устройства не найдены. Подключите по USB или введите IP выше.", + "templates.title": "Шаблоны Движков", + "templates.description": "Шаблоны захвата определяют, как захватывается экран. Каждый шаблон использует определённый движок захвата (MSS, DXcam, WGC) с настраиваемыми параметрами. Назначайте шаблоны устройствам для оптимальной производительности.", + "templates.loading": "Загрузка шаблонов...", + "templates.empty": "Шаблоны захвата не настроены", + "templates.add": "Добавить Шаблон Движка", + "templates.edit": "Редактировать Шаблон Движка", + "templates.name": "Имя Шаблона:", + "templates.name.placeholder": "Мой Пользовательский Шаблон", + "templates.description.label": "Описание (необязательно):", + "templates.description.placeholder": "Опишите этот шаблон...", + "templates.engine": "Движок Захвата:", + "templates.engine.hint": "Выберите технологию захвата экрана", + "templates.engine.select": "Выберите движок...", + "templates.engine.unavailable": "Недоступен", + "templates.engine.unavailable.hint": "Этот движок недоступен в вашей системе", + "templates.engine.mss.desc": "Кроссплатформенный, чистый Python", + "templates.engine.dxcam.desc": "DirectX, низкая задержка", + "templates.engine.bettercam.desc": "DirectX, высокая производительность", + "templates.engine.camera.desc": "Захват USB/IP камеры", + "templates.engine.scrcpy.desc": "Зеркалирование экрана Android", + "templates.engine.wgc.desc": "Windows Graphics Capture", + "templates.config": "Конфигурация", + "templates.config.show": "Показать конфигурацию", + "templates.config.none": "Нет дополнительных настроек", + "templates.config.default": "По умолчанию", + "templates.config.camera_backend.auto": "Автовыбор лучшего бэкенда", + "templates.config.camera_backend.dshow": "Windows DirectShow", + "templates.config.camera_backend.msmf": "Windows Media Foundation", + "templates.config.camera_backend.v4l2": "Linux Video4Linux2", + "templates.created": "Шаблон успешно создан", + "templates.updated": "Шаблон успешно обновлён", + "templates.deleted": "Шаблон успешно удалён", + "templates.delete.confirm": "Вы уверены, что хотите удалить этот шаблон?", + "templates.error.load": "Не удалось загрузить шаблоны", + "templates.error.engines": "Не удалось загрузить движки", + "templates.error.required": "Пожалуйста, заполните все обязательные поля", + "templates.error.delete": "Не удалось удалить шаблон", + "templates.test.title": "Тест Захвата", + "templates.test.description": "Протестируйте этот шаблон перед сохранением, чтобы увидеть предпросмотр захвата и метрики производительности.", + "templates.test.display": "Дисплей:", + "templates.test.display.select": "Выберите дисплей...", + "templates.test.duration": "Длительность Захвата (с):", + "templates.test.border_width": "Ширина Границы (px):", + "templates.test.run": "Запустить", + "templates.test.running": "Выполняется тест...", + "templates.test.results.preview": "Полный Предпросмотр Захвата", + "templates.test.results.borders": "Извлечение Границ", + "templates.test.results.top": "Сверху", + "templates.test.results.right": "Справа", + "templates.test.results.bottom": "Снизу", + "templates.test.results.left": "Слева", + "templates.test.results.performance": "Производительность", + "templates.test.results.capture_time": "Захват", + "templates.test.results.extraction_time": "Извлечение", + "templates.test.results.total_time": "Всего", + "templates.test.results.max_fps": "Макс. FPS", + "templates.test.results.duration": "Длительность", + "templates.test.results.frame_count": "Кадры", + "templates.test.results.actual_fps": "Факт. FPS", + "templates.test.results.avg_capture_time": "Средн. Захват", + "templates.test.results.resolution": "Разрешение:", + "templates.test.error.no_engine": "Пожалуйста, выберите движок захвата", + "templates.test.error.no_display": "Пожалуйста, выберите дисплей", + "templates.test.error.failed": "Тест не удался", + "devices.title": "Устройства", + "device.select_type": "Выберите тип устройства", + "devices.add": "Добавить Новое Устройство", + "devices.loading": "Загрузка устройств...", + "devices.none": "Устройства не настроены", + "devices.failed": "Не удалось загрузить устройства", + "devices.wled_config": "Конфигурация WLED:", + "devices.wled_note": "Настройте ваше WLED устройство (эффекты, сегменты, порядок цветов, ограничения питания и т.д.) используя", + "devices.wled_link": "официальное приложение WLED", + "devices.wled_note_or": "или встроенный", + "devices.wled_webui_link": "веб-интерфейс WLED", + "devices.wled_note_webui": "(откройте IP устройства в браузере).", + "devices.wled_note2": "Этот контроллер отправляет данные о цвете пикселей и управляет яркостью для каждого устройства.", + "device.scan": "Автопоиск", + "device.scan.empty": "Устройства не найдены", + "device.scan.error": "Ошибка сканирования сети", + "device.scan.already_added": "Уже добавлено", + "device.scan.selected": "Устройство выбрано", + "device.type": "Тип устройства:", + "device.type.hint": "Выберите тип LED контроллера", + "device.type.wled": "WLED", + "device.type.wled.desc": "WiFi LED контроллер по HTTP/UDP", + "device.type.adalight": "Adalight", + "device.type.adalight.desc": "Серийный протокол для Arduino", + "device.type.ambiled": "AmbiLED", + "device.type.ambiled.desc": "Серийный протокол AmbiLED", + "device.type.mqtt": "MQTT", + "device.type.mqtt.desc": "Отправка LED данных через MQTT брокер", + "device.type.ws": "WebSocket", + "device.type.ws.desc": "Стриминг LED данных через WebSocket", + "device.type.openrgb": "OpenRGB", + "device.type.openrgb.desc": "Управление RGB через OpenRGB", + "device.type.dmx": "DMX", + "device.type.dmx.desc": "Art-Net / sACN (E1.31) сценическое освещение", + "device.type.mock": "Mock", + "device.type.mock.desc": "Виртуальное устройство для тестов", + "device.type.espnow": "ESP-NOW", + "device.type.espnow.desc": "Ultra-low-latency via ESP32 gateway", + "device.type.hue": "Philips Hue", + "device.type.hue.desc": "Hue Entertainment API streaming", + "device.type.usbhid": "USB HID", + "device.type.usbhid.desc": "USB RGB peripherals (keyboards, mice)", + "device.type.spi": "SPI Direct", + "device.type.spi.desc": "Raspberry Pi GPIO/SPI LED strips", + "device.type.chroma": "Razer Chroma", + "device.type.chroma.desc": "Razer peripherals via Chroma SDK", + "device.type.gamesense": "SteelSeries", + "device.type.gamesense.desc": "SteelSeries peripherals via GameSense", + "device.chroma.device_type": "Peripheral Type:", + "device.chroma.device_type.hint": "Which Razer peripheral to control via Chroma SDK", + "device.gamesense.device_type": "Peripheral Type:", + "device.gamesense.device_type.hint": "Which SteelSeries peripheral to control via GameSense", + "device.espnow.peer_mac": "Peer MAC:", + "device.espnow.peer_mac.hint": "MAC address of the remote ESP32 receiver (e.g. AA:BB:CC:DD:EE:FF)", + "device.espnow.channel": "WiFi Channel:", + "device.espnow.channel.hint": "WiFi channel (1-14). Must match the receiver's channel.", + "device.hue.url": "Bridge IP:", + "device.hue.url.hint": "IP address of your Hue bridge", + "device.hue.username": "Bridge Username:", + "device.hue.username.hint": "Hue bridge application key from pairing", + "device.hue.client_key": "Client Key:", + "device.hue.client_key.hint": "Entertainment API client key (hex string from pairing)", + "device.hue.group_id": "Entertainment Group:", + "device.hue.group_id.hint": "Entertainment configuration ID from your Hue bridge", + "device.usbhid.url": "VID:PID:", + "device.usbhid.url.hint": "USB Vendor:Product ID in hex (e.g. 1532:0084)", + "device.spi.url": "GPIO/SPI Path:", + "device.spi.url.hint": "GPIO pin or SPI device path (e.g. spi://gpio:18)", + "device.spi.speed": "SPI Speed (Hz):", + "device.spi.speed.hint": "SPI clock speed. 800000 Hz for WS2812, 2400000 Hz for APA102.", + "device.spi.led_type": "LED Chipset:", + "device.spi.led_type.hint": "Type of addressable LED strip connected to the GPIO/SPI pin", + "device.spi.led_type.ws2812b.desc": "Most common, 800 KHz data, 3-wire RGB", + "device.spi.led_type.ws2812.desc": "Original WS2812, 800 KHz, 3-wire RGB", + "device.spi.led_type.ws2811.desc": "External driver IC, 400 KHz, 12V strips", + "device.spi.led_type.sk6812.desc": "Samsung LED, 800 KHz, 3-wire RGB", + "device.spi.led_type.sk6812_rgbw.desc": "SK6812 with dedicated white channel", + "device.gamesense.peripheral.keyboard": "Keyboard", + "device.gamesense.peripheral.keyboard.desc": "Per-key RGB illumination", + "device.gamesense.peripheral.mouse": "Mouse", + "device.gamesense.peripheral.mouse.desc": "Mouse RGB zones", + "device.gamesense.peripheral.headset": "Headset", + "device.gamesense.peripheral.headset.desc": "Headset earcup lighting", + "device.gamesense.peripheral.mousepad": "Mousepad", + "device.gamesense.peripheral.mousepad.desc": "Mousepad edge lighting zones", + "device.gamesense.peripheral.indicator": "Indicator", + "device.gamesense.peripheral.indicator.desc": "OLED/LED status indicator", + "device.dmx_protocol": "Протокол DMX:", + "device.dmx_protocol.hint": "Art-Net использует UDP порт 6454, sACN (E1.31) — UDP порт 5568", + "device.dmx_protocol.artnet.desc": "UDP unicast, порт 6454", + "device.dmx_protocol.sacn.desc": "Multicast/unicast, порт 5568", + "device.dmx_start_universe": "Начальный Universe:", + "device.dmx_start_universe.hint": "Первый DMX-юниверс (0-32767). Дополнительные юниверсы используются автоматически при >170 светодиодах.", + "device.dmx_start_channel": "Начальный канал:", + "device.dmx_start_channel.hint": "Первый DMX-канал в юниверсе (1-512)", + "device.dmx.url": "IP адрес:", + "device.dmx.url.hint": "IP адрес DMX-узла (напр. 192.168.1.50)", + "device.dmx.url.placeholder": "192.168.1.50", + "device.serial_port": "Серийный порт:", + "device.serial_port.hint": "Выберите COM порт устройства Adalight", + "device.serial_port.none": "Серийные порты не найдены", + "device.serial_port.select": "Выберите порт...", + "device.led_count_manual.hint": "Количество светодиодов на ленте (должно совпадать с вашим скетчем Arduino)", + "device.baud_rate": "Скорость порта:", + "device.baud_rate.hint": "Скорость серийного соединения. Выше = больше FPS, но требует соответствия скетчу Arduino.", + "device.led_type": "Тип LED:", + "device.led_type.hint": "RGB (3 канала) или RGBW (4 канала с выделенным белым)", + "device.send_latency": "Задержка отправки (мс):", + "device.send_latency.hint": "Имитация сетевой/серийной задержки на кадр в миллисекундах", + "device.css_processing_template": "Шаблон Обработки Полос:", + "device.css_processing_template.hint": "Шаблон обработки по умолчанию, применяемый ко всем цветовым полосам на этом устройстве", + "device.mqtt_topic": "MQTT Топик:", + "device.mqtt_topic.hint": "MQTT топик для публикации пиксельных данных (напр. mqtt://ledgrab/device/name)", + "device.mqtt_topic.placeholder": "mqtt://ledgrab/device/гостиная", + "device.ws_url": "URL подключения:", + "device.ws_url.hint": "WebSocket URL для подключения клиентов и получения LED данных", + "device.openrgb.url": "OpenRGB URL:", + "device.openrgb.url.hint": "Адрес сервера OpenRGB (напр. openrgb://localhost:6742/0)", + "device.openrgb.zone": "Зоны:", + "device.openrgb.zone.hint": "Выберите зоны LED для управления (оставьте все неотмеченными для всех зон)", + "device.openrgb.zone.loading": "Загрузка зон…", + "device.openrgb.zone.error": "Не удалось загрузить зоны", + "device.openrgb.mode": "Режим зон:", + "device.openrgb.mode.hint": "Объединённый — все зоны как одна непрерывная LED-лента. Раздельный — каждая зона независимо отображает полный эффект.", + "device.openrgb.mode.combined": "Объединённая лента", + "device.openrgb.mode.separate": "Независимые зоны", + "device.openrgb.added_multiple": "Добавлено {count} устройств", + "device.url.hint": "IP адрес или имя хоста устройства (напр. http://192.168.1.100)", + "device.name": "Имя Устройства:", + "device.name.placeholder": "ТВ в Гостиной", + "device.url": "URL:", + "device.url.placeholder": "http://192.168.1.100", + "device.led_count": "Количество Светодиодов:", + "device.led_count.hint": "Количество светодиодов, настроенных в устройстве", + "device.led_count.hint.auto": "Автоматически определяется из устройства", + "device.button.add": "Добавить Устройство", + "device.button.start": "Запустить", + "device.button.stop": "Остановить", + "device.button.settings": "Основные настройки", + "device.button.capture_settings": "Настройки захвата", + "device.button.calibrate": "Калибровка", + "device.button.remove": "Удалить", + "device.button.webui": "Открыть веб-интерфейс устройства", + "device.button.power_off": "Выключить", + "device.button.ping": "Пинг устройства", + "device.ping.online": "Онлайн ({ms}мс)", + "device.ping.offline": "Устройство недоступно", + "device.ping.error": "Ошибка пинга", + "device.power.off_success": "Устройство выключено", + "device.status.connected": "Подключено", + "device.status.disconnected": "Отключено", + "device.status.error": "Ошибка", + "device.status.processing": "Обработка", + "device.status.idle": "Ожидание", + "device.fps": "FPS:", + "device.display": "Дисплей:", + "device.remove.confirm": "Вы уверены, что хотите удалить это устройство?", + "device.added": "Устройство успешно добавлено", + "device.removed": "Устройство удалено", + "device.started": "Обработка запущена", + "device.stopped": "Обработка остановлена", + "device.metrics.actual_fps": "Факт. FPS", + "device.metrics.current_fps": "Текущ. FPS", + "device.metrics.target_fps": "Целев. FPS", + "device.metrics.potential_fps": "Потенц. FPS", + "device.metrics.frames": "Кадры", + "device.metrics.frames_skipped": "Пропущено", + "device.metrics.keepalive": "Keepalive", + "device.metrics.errors": "Ошибки", + "device.metrics.uptime": "Время работы", + "device.metrics.timing": "Тайминг пайплайна:", + "device.metrics.device_fps": "Частота обновления устройства", + "device.health.online": "Онлайн", + "device.health.offline": "Недоступен", + "device.health.streaming_unreachable": "Недоступен во время стриминга", + "device.health.checking": "Проверка...", + "device.last_seen.label": "Последний раз", + "device.last_seen.just_now": "только что", + "device.last_seen.seconds": "%d с назад", + "device.last_seen.minutes": "%d мин назад", + "device.last_seen.hours": "%d ч назад", + "device.last_seen.days": "%d д назад", + "device.tutorial.start": "Начать обучение", + "device.tip.metadata": "Информация об устройстве (кол-во LED, тип, цветовые каналы) определяется автоматически", + "device.tip.brightness": "Перетащите для регулировки яркости", + "device.tip.start": "Запуск или остановка захвата экрана", + "device.tip.settings": "Основные настройки устройства (имя, URL, интервал проверки)", + "device.tip.capture_settings": "Настройки захвата (дисплей, шаблон захвата)", + "device.tip.calibrate": "Калибровка позиций LED, направления и зоны покрытия", + "device.tip.webui": "Открыть встроенный веб-интерфейс устройства для расширенной настройки", + "device.tip.add": "Нажмите, чтобы добавить новое LED устройство", + "settings.title": "Настройки", + "settings.tab.general": "Основные", + "settings.tab.backup": "Бэкап", + "settings.tab.mqtt": "MQTT", + "settings.tab.appearance": "Оформление", + "settings.logs.open_viewer": "Открыть логи", + "settings.external_url.label": "Внешний URL", + "settings.external_url.hint": "Если указан, этот базовый URL используется в URL-ах вебхуков и других пользовательских ссылках вместо автоопределённого локального IP. Пример: https://myserver.example.com:8080", + "settings.external_url.placeholder": "https://myserver.example.com:8080", + "settings.external_url.save": "Сохранить", + "settings.external_url.saved": "Внешний URL сохранён", + "settings.external_url.save_error": "Не удалось сохранить внешний URL", + "settings.general.title": "Основные Настройки", + "settings.capture.title": "Настройки Захвата", + "settings.capture.saved": "Настройки захвата обновлены", + "settings.capture.failed": "Не удалось сохранить настройки захвата", + "settings.brightness": "Яркость:", + "settings.brightness.hint": "Общая яркость для этого устройства (0-100%)", + "settings.url.hint": "IP адрес или имя хоста устройства", + "settings.display_index": "Дисплей:", + "settings.display_index.hint": "Какой экран захватывать для этого устройства", + "settings.fps": "Целевой FPS:", + "settings.fps.hint": "Целевая частота кадров (10-90)", + "settings.capture_template": "Шаблон Движка:", + "settings.capture_template.hint": "Движок захвата экрана и конфигурация для этого устройства", + "settings.button.cancel": "Отмена", + "settings.health_interval": "Интервал Проверки (с):", + "settings.health_interval.hint": "Как часто проверять статус устройства (5-600 секунд)", + "settings.auto_shutdown": "Авто-восстановление:", + "settings.auto_shutdown.hint": "Восстанавливать устройство в режим ожидания при остановке целей или сервера", + "settings.button.save": "Сохранить Изменения", + "settings.saved": "Настройки успешно сохранены", + "settings.failed": "Не удалось сохранить настройки", + "calibration.title": "Калибровка Светодиодов", + "calibration.tip.led_count": "Укажите количество LED на каждой стороне", + "calibration.tip.start_corner": "Нажмите на угол для выбора стартовой позиции", + "calibration.tip.direction": "Переключение направления ленты (по часовой / против часовой)", + "calibration.tip.offset": "Смещение LED — расстояние от LED 0 до стартового угла", + "calibration.tip.span": "Перетащите зелёные полосы для настройки зоны покрытия", + "calibration.tip.test": "Нажмите на край для теста LED", + "calibration.tip.overlay": "Включите оверлей для отображения позиций и нумерации LED на мониторе", + "calibration.tip.toggle_inputs": "Нажмите на общее количество LED для скрытия боковых полей", + "calibration.tip.border_width": "Сколько пикселей от края экрана использовать для цветов LED", + "calibration.tip.skip_leds_start": "Пропуск LED в начале ленты — пропущенные LED остаются выключенными", + "calibration.tip.skip_leds_end": "Пропуск LED в конце ленты — пропущенные LED остаются выключенными", + "tour.welcome": "Добро пожаловать в LED Grab! Этот краткий тур познакомит вас с интерфейсом. Используйте стрелки или кнопки для навигации.", + "tour.dashboard": "Дашборд — обзор запущенных целей, автоматизаций и состояния устройств.", + "tour.targets": "Цели — добавляйте WLED-устройства, настраивайте LED-цели с захватом и калибровкой.", + "tour.sources": "Источники — управление шаблонами захвата, источниками изображений, звука и цветовых полос.", + "tour.graph": "Граф — визуальный обзор всех сущностей и их связей. Перетаскивайте порты для соединения, правый клик по связям для отключения.", + "tour.automations": "Автоматизации — автоматизируйте переключение сцен по расписанию, звуку или значениям.", + "tour.settings": "Настройки — резервное копирование и восстановление конфигурации.", + "tour.api": "API Документация — интерактивная документация REST API на базе Swagger.", + "tour.search": "Поиск — быстрый поиск и переход к любому объекту по Ctrl+K.", + "tour.theme": "Тема — переключение между тёмной и светлой темой.", + "tour.accent": "Цвет акцента — настройте цвет интерфейса по своему вкусу.", + "tour.language": "Язык — выберите предпочитаемый язык интерфейса.", + "tour.restart": "Запустить тур заново", + "tour.dash.perf": "Производительность — графики FPS в реальном времени, метрики задержки и интервал опроса.", + "tour.dash.running": "Запущенные цели — метрики стриминга и быстрая остановка.", + "tour.dash.stopped": "Остановленные цели — готовы к запуску одним нажатием.", + "tour.dash.automations": "Автоматизации — статус активных автоматизаций и быстрое включение/выключение.", + "tour.tgt.led_tab": "LED — стандартные LED-цели с настройкой устройств и цветовых полос.", + "tour.tgt.devices": "Устройства — ваши LED-контроллеры, найденные в сети.", + "tour.tgt.css": "Цветовые полосы — определите, как области экрана соответствуют сегментам LED.", + "tour.tgt.targets": "LED-цели — объедините устройство, цветовую полосу и источник захвата для стриминга.", + "tour.tgt.kc_tab": "Key Colors — альтернативный тип цели с подбором цветов вместо пиксельного маппинга.", + "tour.src.raw": "Raw — источники захвата экрана с ваших дисплеев.", + "tour.src.templates": "Шаблоны захвата — переиспользуемые конфигурации (разрешение, FPS, обрезка).", + "tour.src.static": "Статичные изображения — тестируйте настройку с файлами изображений.", + "tour.src.processed": "Обработка — применяйте эффекты: размытие, яркость, цветокоррекция.", + "tour.src.color_strip": "Цветовые полосы — определяют, как области экрана сопоставляются с LED-сегментами.", + "tour.src.audio": "Аудио — анализ микрофона или системного звука для реактивных LED-эффектов.", + "tour.src.value": "Значения — числовые источники данных для условий автоматизаций.", + "tour.src.sync": "Синхро-часы — общие таймеры для синхронизации анимаций между несколькими источниками.", + "tour.auto.list": "Автоматизации — автоматизируйте активацию сцен по времени, звуку или значениям.", + "tour.auto.add": "Нажмите + для создания новой автоматизации с условиями и сценой для активации.", + "tour.auto.card": "Каждая карточка показывает статус автоматизации, условия и кнопки управления.", + "tour.auto.scenes_list": "Сцены — сохранённые состояния системы, которые автоматизации могут активировать или вы можете применить вручную.", + "tour.auto.scenes_add": "Нажмите + для захвата текущего состояния системы как нового пресета сцены.", + "tour.auto.scenes_card": "Каждая карточка сцены показывает количество целей/устройств. Нажмите для редактирования, перезахвата или активации.", + "calibration.tutorial.start": "Начать обучение", + "calibration.overlay_toggle": "Оверлей", + "calibration.start_position": "Начальная Позиция:", + "calibration.position.bottom_left": "Нижний Левый", + "calibration.position.bottom_right": "Нижний Правый", + "calibration.position.top_left": "Верхний Левый", + "calibration.position.top_right": "Верхний Правый", + "calibration.direction": "Направление:", + "calibration.direction.clockwise": "По Часовой Стрелке", + "calibration.direction.counterclockwise": "Против Часовой Стрелки", + "calibration.leds.top": "Светодиодов Сверху:", + "calibration.leds.right": "Светодиодов Справа:", + "calibration.leds.bottom": "Светодиодов Снизу:", + "calibration.leds.left": "Светодиодов Слева:", + "calibration.offset": "Смещение LED:", + "calibration.offset.hint": "Расстояние от физического LED 0 до стартового угла (по направлению ленты)", + "calibration.skip_start": "Пропуск LED (начало):", + "calibration.skip_start.hint": "Количество LED, которые будут выключены в начале ленты (0 = нет)", + "calibration.skip_end": "Пропуск LED (конец):", + "calibration.skip_end.hint": "Количество LED, которые будут выключены в конце ленты (0 = нет)", + "calibration.border_width": "Граница (px):", + "calibration.border_width.hint": "Сколько пикселей от края экрана выбирать для цвета LED (1-100)", + "calibration.button.cancel": "Отмена", + "calibration.button.save": "Сохранить", + "calibration.saved": "Калибровка сохранена", + "calibration.failed": "Не удалось сохранить калибровку", + "server.healthy": "Сервер онлайн", + "server.offline": "Сервер офлайн", + "error.unauthorized": "Не авторизован - пожалуйста, войдите", + "error.network": "Сетевая ошибка", + "error.unknown": "Произошла ошибка", + "modal.discard_changes": "У вас есть несохранённые изменения. Отменить их?", + "confirm.title": "Подтверждение", + "confirm.yes": "Да", + "confirm.no": "Нет", + "confirm.stop_all": "Остановить все запущенные цели?", + "confirm.turn_off_device": "Выключить это устройство?", + "common.loading": "Загрузка...", + "common.delete": "Удалить", + "common.remove": "Убрать", + "common.edit": "Редактировать", + "common.clone": "Клонировать", + "common.none": "Нет", + "common.none_no_cspt": "Нет (без шаблона обработки)", + "common.none_no_input": "Нет (без источника)", + "common.none_own_speed": "Нет (своя скорость)", + "common.undo": "Отменить", + "validation.required": "Обязательное поле", + "bulk.processing": "Обработка…", + "api.error.timeout": "Превышено время ожидания — попробуйте снова", + "api.error.network": "Ошибка сети — проверьте подключение", + "palette.search": "Поиск…", + "section.filter.placeholder": "Фильтр...", + "section.filter.reset": "Очистить фильтр", + "tags.label": "Теги", + "tags.hint": "Назначьте теги для группировки и фильтрации карточек", + "tags.placeholder": "Добавить тег...", + "section.expand_all": "Развернуть все секции", + "section.collapse_all": "Свернуть все секции", + "streams.title": "Источники", + "streams.description": "Источники определяют конвейер захвата. Сырой источник захватывает экран с помощью шаблона захвата. Обработанный источник применяет постобработку к другому источнику. Назначайте источники устройствам.", + "streams.group.raw": "Источники", + "streams.group.raw_templates": "Шаблоны движка", + "streams.group.processed": "Источники", + "streams.group.proc_templates": "Шаблоны фильтров", + "streams.group.css_processing": "Шаблоны Обработки", + "streams.group.color_strip": "Цветовые Полосы", + "streams.group.audio": "Аудио", + "streams.group.audio_templates": "Аудио шаблоны", + "streams.section.streams": "Источники", + "streams.add": "Добавить Источник", + "streams.add.raw": "Добавить Захват Экрана", + "streams.add.processed": "Добавить Обработанный", + "streams.edit": "Редактировать Источник", + "streams.edit.raw": "Редактировать Захват Экрана", + "streams.edit.processed": "Редактировать Обработанный Источник", + "streams.name": "Имя Источника:", + "streams.name.placeholder": "Мой Источник", + "streams.type": "Тип:", + "streams.type.raw": "Захват экрана", + "streams.type.processed": "Обработанный", + "streams.display": "Дисплей:", + "streams.display.hint": "Какой экран захватывать", + "streams.capture_template": "Шаблон Движка:", + "streams.capture_template.hint": "Шаблон движка, определяющий способ захвата экрана", + "streams.target_fps": "Целевой FPS:", + "streams.target_fps.hint": "Целевое количество кадров в секунду (1-90)", + "streams.source": "Источник:", + "streams.source.hint": "Источник, к которому применяются фильтры обработки", + "streams.pp_template": "Шаблон Фильтра:", + "streams.pp_template.hint": "Шаблон фильтра для применения к источнику", + "streams.description_label": "Описание (необязательно):", + "streams.description_placeholder": "Опишите этот источник...", + "streams.created": "Источник успешно создан", + "streams.updated": "Источник успешно обновлён", + "streams.deleted": "Источник успешно удалён", + "streams.delete.confirm": "Вы уверены, что хотите удалить этот источник?", + "streams.modal.loading": "Загрузка...", + "streams.error.load": "Не удалось загрузить источники", + "streams.error.required": "Пожалуйста, заполните все обязательные поля", + "streams.error.delete": "Не удалось удалить источник", + "streams.test.title": "Тест Источника", + "streams.test.run": "Запустить", + "streams.test.running": "Тестирование источника...", + "streams.test.duration": "Длительность Захвата (с):", + "streams.test.error.failed": "Тест источника не удался", + "postprocessing.title": "Шаблоны Фильтров", + "postprocessing.description": "Шаблоны обработки определяют фильтры изображений и цветокоррекцию. Назначайте их обработанным источникам для единообразной постобработки на всех устройствах.", + "postprocessing.add": "Добавить Шаблон Фильтра", + "postprocessing.edit": "Редактировать Шаблон Фильтра", + "postprocessing.name": "Имя Шаблона:", + "postprocessing.name.placeholder": "Мой Шаблон Фильтра", + "filters.select_type": "Выберите тип фильтра...", + "filters.add": "Добавить фильтр", + "filters.remove": "Удалить", + "filters.drag_to_reorder": "Перетащите для изменения порядка", + "filters.empty": "Фильтры не добавлены. Используйте селектор ниже для добавления.", + "filters.brightness": "Яркость", + "filters.brightness.desc": "Регулировка общей яркости изображения", + "filters.saturation": "Насыщенность", + "filters.saturation.desc": "Усиление или снижение интенсивности цвета", + "filters.gamma": "Гамма", + "filters.gamma.desc": "Нелинейная коррекция кривой яркости", + "filters.downscaler": "Уменьшение", + "filters.downscaler.desc": "Снижение разрешения для быстрой обработки", + "filters.pixelate": "Пикселизация", + "filters.pixelate.desc": "Мозаичное усреднение блоков", + "filters.auto_crop": "Авто Обрезка", + "filters.auto_crop.desc": "Удаление чёрных полос из леттербокса", + "filters.flip": "Отражение", + "filters.flip.desc": "Зеркальное отражение по горизонтали или вертикали", + "filters.color_correction": "Цветокоррекция", + "filters.color_correction.desc": "Баланс белого и цветовая температура", + "filters.filter_template": "Шаблон фильтров", + "filters.filter_template.desc": "Встроить другой шаблон обработки", + "filters.css_filter_template": "Шаблон Фильтра Полос", + "filters.css_filter_template.desc": "Встроить другой шаблон обработки полос", + "filters.frame_interpolation": "Интерполяция кадров", + "filters.frame_interpolation.desc": "Сглаживание между кадрами", + "filters.noise_gate": "Шумоподавление", + "filters.noise_gate.desc": "Подавление малых изменений цвета ниже порога", + "filters.palette_quantization": "Квантизация палитры", + "filters.palette_quantization.desc": "Сокращение цветов до ограниченной палитры", + "filters.reverse": "Реверс", + "filters.reverse.desc": "Изменить порядок светодиодов на обратный", + "postprocessing.description_label": "Описание (необязательно):", + "postprocessing.description_placeholder": "Опишите этот шаблон...", + "postprocessing.created": "Шаблон успешно создан", + "postprocessing.updated": "Шаблон успешно обновлён", + "postprocessing.deleted": "Шаблон успешно удалён", + "postprocessing.delete.confirm": "Вы уверены, что хотите удалить этот шаблон фильтра?", + "postprocessing.error.load": "Не удалось загрузить шаблоны фильтров", + "postprocessing.error.required": "Пожалуйста, заполните все обязательные поля", + "postprocessing.error.delete": "Не удалось удалить шаблон фильтра", + "postprocessing.config.show": "Показать настройки", + "postprocessing.test.title": "Тест шаблона фильтра", + "postprocessing.test.source_stream": "Источник:", + "postprocessing.test.running": "Тестирование шаблона фильтра...", + "postprocessing.test.error.no_stream": "Пожалуйста, выберите источник", + "postprocessing.test.error.failed": "Тест шаблона фильтра не удался", + "css_processing.title": "Шаблоны Обработки Полос", + "css_processing.add": "Добавить Шаблон Обработки Полос", + "css_processing.edit": "Редактировать Шаблон Обработки Полос", + "css_processing.name": "Имя Шаблона:", + "css_processing.name_placeholder": "Мой Шаблон Обработки Полос", + "css_processing.description_label": "Описание (необязательно):", + "css_processing.description_placeholder": "Опишите этот шаблон...", + "css_processing.created": "Шаблон обработки полос создан", + "css_processing.updated": "Шаблон обработки полос обновлён", + "css_processing.deleted": "Шаблон обработки полос удалён", + "css_processing.delete.confirm": "Вы уверены, что хотите удалить этот шаблон обработки полос?", + "css_processing.error.required": "Заполните все обязательные поля", + "css_processing.error.load": "Ошибка загрузки шаблона обработки полос", + "css_processing.error.delete": "Ошибка удаления шаблона обработки полос", + "css_processing.error.clone_failed": "Не удалось клонировать шаблон обработки полос", + "device.button.stream_selector": "Настройки источника", + "device.stream_settings.title": "Настройки источника", + "device.stream_selector.label": "Источник:", + "device.stream_selector.hint": "Выберите источник, определяющий что это устройство захватывает и обрабатывает", + "device.stream_selector.none": "-- Источник не назначен --", + "device.stream_selector.saved": "Настройки источника обновлены", + "device.stream_settings.border_width": "Ширина границы (px):", + "device.stream_settings.border_width_hint": "Сколько пикселей от края экрана выбирать для цвета LED (1-100)", + "device.stream_settings.interpolation": "Режим интерполяции:", + "device.stream_settings.interpolation.average": "Среднее", + "device.stream_settings.interpolation.median": "Медиана", + "device.stream_settings.interpolation.dominant": "Доминантный", + "device.stream_settings.interpolation_hint": "Как вычислять цвет LED из выбранных пикселей", + "device.stream_settings.smoothing": "Сглаживание:", + "device.stream_settings.smoothing_hint": "Временное смешивание между кадрами (0=нет, 1=полное). Уменьшает мерцание.", + "device.tip.stream_selector": "Настройки источника и проекции LED для этого устройства", + "streams.group.static_image": "Статические", + "streams.add.static_image": "Добавить статическое изображение (источник)", + "streams.edit.static_image": "Редактировать статическое изображение (источник)", + "streams.type.static_image": "Статическое изображение", + "streams.group.video": "Видео", + "streams.add.video": "Добавить видеоисточник", + "streams.edit.video": "Редактировать видеоисточник", + "picture_source.type.video": "Видео", + "picture_source.type.video.desc": "Потоковые кадры из загруженного видео", + "picture_source.video.loop": "Зацикливание:", + "picture_source.video.speed": "Скорость воспроизведения:", + "picture_source.video.start_time": "Время начала (с):", + "picture_source.video.end_time": "Время окончания (с):", + "picture_source.video.resolution_limit": "Макс. ширина (px):", + "picture_source.video.resolution_limit.hint": "Уменьшение видео при декодировании для производительности", + "streams.image_asset": "Изображение:", + "streams.image_asset.select": "Выберите изображение…", + "streams.image_asset.search": "Поиск изображений…", + "streams.video_asset": "Видео:", + "streams.video_asset.select": "Выберите видео…", + "streams.video_asset.search": "Поиск видео…", + "targets.title": "Цели", + "targets.description": "Цели связывают источники цветовых полос с устройствами вывода. Каждая цель ссылается на устройство и источник цветовой полосы.", + "targets.subtab.wled": "LED", + "targets.subtab.led": "LED", + "targets.section.devices": "Устройства", + "targets.section.color_strips": "Источники цветовых полос", + "targets.section.targets": "Цели", + "targets.section.specific_settings": "Специальные настройки", + "targets.section.advanced": "Расширенные", + "targets.add": "Добавить Цель", + "targets.edit": "Редактировать Цель", + "targets.loading": "Загрузка целей...", + "targets.none": "Цели не настроены", + "targets.failed": "Не удалось загрузить цели", + "targets.name": "Имя Цели:", + "targets.name.placeholder": "Моя Цель", + "targets.device": "Устройство:", + "targets.device.hint": "Выберите LED устройство для передачи данных", + "targets.device.none": "-- Выберите устройство --", + "targets.color_strip_source": "Источник цветовой полосы:", + "targets.color_strip_source.hint": "Выберите источник цветовой полосы, который предоставляет цвета LED для этой цели", + "targets.no_css": "Нет источника", + "targets.source": "Источник:", + "targets.source.hint": "Какой источник изображения захватывать и обрабатывать", + "targets.source.none": "-- Источник не назначен --", + "targets.metrics.pipeline": "Детали конвейера", + "targets.fps": "Целевой FPS:", + "targets.fps.hint": "Целевая частота кадров для захвата и обновления LED (1-90)", + "targets.fps.rec": "Макс. аппаратный ≈ {fps} fps ({leds} LED)", + "targets.border_width": "Ширина границы (px):", + "targets.border_width.hint": "Сколько пикселей от края экрана выбирать для цвета LED (1-100)", + "targets.interpolation": "Режим интерполяции:", + "targets.interpolation.hint": "Как вычислять цвет LED из выбранных пикселей", + "targets.interpolation.average": "Среднее", + "targets.interpolation.median": "Медиана", + "targets.interpolation.dominant": "Доминантный", + "targets.smoothing": "Сглаживание:", + "targets.smoothing.hint": "Временное смешивание между кадрами (0=нет, 1=полное). Уменьшает мерцание.", + "targets.keepalive_interval": "Интервал поддержания связи:", + "targets.keepalive_interval.hint": "Как часто повторно отправлять последний кадр при статичном источнике для удержания устройства в режиме live (0.5-5.0с)", + "targets.created": "Цель успешно создана", + "targets.updated": "Цель успешно обновлена", + "targets.deleted": "Цель успешно удалена", + "targets.delete.confirm": "Вы уверены, что хотите удалить эту цель?", + "targets.error.load": "Не удалось загрузить цели", + "targets.error.required": "Пожалуйста, заполните все обязательные поля", + "targets.error.name_required": "Введите название цели", + "targets.error.delete": "Не удалось удалить цель", + "targets.button.start": "Запустить", + "targets.button.stop": "Остановить", + "targets.status.processing": "Обработка", + "targets.status.idle": "Ожидание", + "targets.status.error": "Ошибка", + "targets.metrics.actual_fps": "Факт. FPS", + "targets.metrics.target_fps": "Целев. FPS", + "targets.metrics.frames": "Кадры", + "targets.metrics.errors": "Ошибки", + "targets.section.pattern_templates": "Шаблоны Паттернов", + "pattern.add": "Добавить Шаблон Паттерна", + "pattern.edit": "Редактировать Шаблон Паттерна", + "pattern.name": "Имя Шаблона:", + "pattern.name.placeholder": "Мой Шаблон Паттерна", + "pattern.description_label": "Описание (необязательно):", + "pattern.description_placeholder": "Опишите этот паттерн...", + "pattern.rectangles": "Прямоугольники", + "pattern.rect.name": "Имя", + "pattern.rect.x": "X", + "pattern.rect.y": "Y", + "pattern.rect.width": "Ш", + "pattern.rect.height": "В", + "pattern.rect.add": "Добавить Прямоугольник", + "pattern.rect.remove": "Удалить", + "pattern.rect.empty": "Прямоугольники не определены. Добавьте хотя бы один.", + "pattern.created": "Шаблон паттерна успешно создан", + "pattern.updated": "Шаблон паттерна успешно обновлён", + "pattern.deleted": "Шаблон паттерна успешно удалён", + "pattern.delete.confirm": "Вы уверены, что хотите удалить этот шаблон паттерна?", + "pattern.delete.referenced": "Невозможно удалить: шаблон используется целью", + "pattern.error.required": "Пожалуйста, заполните все обязательные поля", + "pattern.visual_editor": "Визуальный Редактор", + "pattern.capture_bg": "Захватить Фон", + "pattern.source_for_bg": "Источник для Фона:", + "pattern.source_for_bg.none": "-- Выберите источник --", + "pattern.delete_selected": "Удалить Выбранный", + "pattern.name.hint": "Описательное имя для этой раскладки прямоугольников", + "pattern.description.hint": "Необязательные заметки о назначении этого паттерна", + "pattern.visual_editor.hint": "Нажмите кнопки + чтобы добавить прямоугольники. Тяните края для изменения размера, тяните внутри для перемещения.", + "pattern.rectangles.hint": "Точная настройка позиций и размеров прямоугольников в координатах (0.0 до 1.0)", + "overlay.toggle": "Переключить наложение на экран", + "overlay.button.show": "Показать визуализацию наложения", + "overlay.button.hide": "Скрыть визуализацию наложения", + "overlay.started": "Визуализация наложения запущена", + "overlay.stopped": "Визуализация наложения остановлена", + "overlay.error.start": "Не удалось запустить наложение", + "overlay.error.stop": "Не удалось остановить наложение", + "dashboard.title": "Обзор", + "dashboard.section.targets": "Цели", + "dashboard.section.running": "Запущенные", + "dashboard.section.stopped": "Остановленные", + "dashboard.no_targets": "Нет настроенных целей", + "dashboard.uptime": "Время работы", + "dashboard.fps": "FPS", + "dashboard.errors": "Ошибки", + "dashboard.device": "Устройство", + "dashboard.stop_all": "Остановить все", + "dashboard.failed": "Не удалось загрузить обзор", + "dashboard.section.automations": "Автоматизации", + "dashboard.section.scenes": "Пресеты сцен", + "dashboard.section.sync_clocks": "Синхронные часы", + "dashboard.targets": "Цели", + "dashboard.section.performance": "Производительность системы", + "dashboard.perf.cpu": "ЦП", + "dashboard.perf.ram": "ОЗУ", + "dashboard.perf.gpu": "ГП", + "dashboard.perf.unavailable": "недоступно", + "dashboard.perf.color": "Цвет графика", + "dashboard.perf.mode.system": "Система", + "dashboard.perf.mode.app": "Приложение", + "dashboard.perf.mode.both": "Оба", + "dashboard.poll_interval": "Интервал обновления", + "automations.title": "Автоматизации", + "automations.empty": "Автоматизации не настроены. Создайте автоматизацию для автоматической активации сцен.", + "automations.add": "Добавить автоматизацию", + "automations.edit": "Редактировать автоматизацию", + "automations.delete.confirm": "Удалить автоматизацию \"{name}\"?", + "automations.name": "Название:", + "automations.name.hint": "Описательное имя для автоматизации", + "automations.name.placeholder": "Моя автоматизация", + "automations.enabled": "Включена:", + "automations.enabled.hint": "Отключённые автоматизации не активируются даже при выполнении условий", + "automations.condition_logic": "Логика условий:", + "automations.condition_logic.hint": "Как объединяются несколько условий: ЛЮБОЕ (ИЛИ) или ВСЕ (И)", + "automations.condition_logic.or": "Любое условие (ИЛИ)", + "automations.condition_logic.and": "Все условия (И)", + "automations.condition_logic.or.desc": "Срабатывает при любом совпадении", + "automations.condition_logic.and.desc": "Срабатывает только при всех", + "automations.conditions": "Условия:", + "automations.conditions.hint": "Правила, определяющие когда автоматизация активируется", + "automations.conditions.add": "Добавить условие", + "automations.conditions.empty": "Нет условий — автоматизация всегда активна когда включена", + "automations.condition.always": "Всегда", + "automations.condition.always.desc": "Всегда активно", + "automations.condition.always.hint": "Автоматизация активируется сразу при включении и остаётся активной.", + "automations.condition.startup": "Автозапуск", + "automations.condition.startup.desc": "При запуске сервера", + "automations.condition.startup.hint": "Активируется при запуске сервера и остаётся активной пока включена.", + "automations.condition.application": "Приложение", + "automations.condition.application.desc": "Приложение запущено", + "automations.condition.application.apps": "Приложения:", + "automations.condition.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)", + "automations.condition.application.browse": "Обзор", + "automations.condition.application.search": "Фильтр процессов...", + "automations.condition.application.no_processes": "Процессы не найдены", + "automations.condition.application.match_type": "Тип соответствия:", + "automations.condition.application.match_type.hint": "Как определять наличие приложения", + "automations.condition.application.match_type.running": "Запущено", + "automations.condition.application.match_type.running.desc": "Процесс активен", + "automations.condition.application.match_type.topmost": "На переднем плане", + "automations.condition.application.match_type.topmost.desc": "Окно в фокусе", + "automations.condition.application.match_type.topmost_fullscreen": "Передний план + ПЭ", + "automations.condition.application.match_type.topmost_fullscreen.desc": "В фокусе + полный экран", + "automations.condition.application.match_type.fullscreen": "Полный экран", + "automations.condition.application.match_type.fullscreen.desc": "Любое полноэкранное", + "automations.condition.time_of_day": "Время суток", + "automations.condition.time_of_day.desc": "Диапазон времени", + "automations.condition.time_of_day.start_time": "Время начала:", + "automations.condition.time_of_day.end_time": "Время окончания:", + "automations.condition.time_of_day.overnight_hint": "Для ночных диапазонов (например 22:00–06:00) укажите время начала позже времени окончания.", + "automations.condition.system_idle": "Бездействие системы", + "automations.condition.system_idle.desc": "Бездействие/активность", + "automations.condition.system_idle.idle_minutes": "Тайм-аут бездействия (минуты):", + "automations.condition.system_idle.mode": "Режим срабатывания:", + "automations.condition.system_idle.when_idle": "При бездействии", + "automations.condition.system_idle.when_active": "При активности", + "automations.condition.display_state": "Состояние дисплея", + "automations.condition.display_state.desc": "Монитор вкл/выкл", + "automations.condition.display_state.state": "Состояние монитора:", + "automations.condition.display_state.on": "Включён", + "automations.condition.display_state.off": "Выключен (спящий режим)", + "automations.condition.mqtt": "MQTT", + "automations.condition.mqtt.desc": "MQTT сообщение", + "automations.condition.mqtt.topic": "Топик:", + "automations.condition.mqtt.payload": "Значение:", + "automations.condition.mqtt.match_mode": "Режим сравнения:", + "automations.condition.mqtt.match_mode.exact": "Точное совпадение", + "automations.condition.mqtt.match_mode.contains": "Содержит", + "automations.condition.mqtt.match_mode.regex": "Регулярное выражение", + "automations.condition.mqtt.hint": "Активировать при получении совпадающего значения по MQTT топику", + "automations.condition.webhook": "Вебхук", + "automations.condition.webhook.desc": "HTTP вызов", + "automations.condition.webhook.hint": "Активировать через HTTP-запрос от внешних сервисов (Home Assistant, IFTTT, curl и т.д.)", + "automations.condition.webhook.url": "URL вебхука:", + "automations.condition.webhook.copy": "Скопировать", + "automations.condition.webhook.copied": "Скопировано!", + "automations.condition.webhook.save_first": "Сначала сохраните автоматизацию для генерации URL вебхука", + "automations.scene": "Сцена:", + "automations.scene.hint": "Пресет сцены для активации при выполнении условий", + "automations.scene.search_placeholder": "Поиск сцен...", + "automations.scene.none_selected": "Нет (без сцены)", + "automations.scene.none_available": "Нет доступных сцен", + "automations.deactivation_mode": "Деактивация:", + "automations.deactivation_mode.hint": "Что происходит, когда условия перестают выполняться", + "automations.deactivation_mode.none": "Ничего", + "automations.deactivation_mode.none.desc": "Оставить текущее состояние", + "automations.deactivation_mode.revert": "Откатить", + "automations.deactivation_mode.revert.desc": "Вернуть предыдущее состояние", + "automations.deactivation_mode.fallback_scene": "Резервная", + "automations.deactivation_mode.fallback_scene.desc": "Активировать резервную сцену", + "automations.deactivation_scene": "Резервная сцена:", + "automations.deactivation_scene.hint": "Сцена для активации при деактивации автоматизации", + "automations.status.active": "Активна", + "automations.status.inactive": "Неактивна", + "automations.status.disabled": "Отключена", + "automations.action.disable": "Отключить", + "automations.last_activated": "Последняя активация", + "automations.logic.and": " И ", + "automations.logic.or": " ИЛИ ", + "automations.logic.all": "ВСЕ", + "automations.logic.any": "ЛЮБОЕ", + "automations.updated": "Автоматизация обновлена", + "automations.created": "Автоматизация создана", + "automations.deleted": "Автоматизация удалена", + "automations.error.name_required": "Введите название", + "automations.error.clone_failed": "Не удалось клонировать автоматизацию", + "scenes.title": "Сцены", + "scenes.add": "Захватить сцену", + "scenes.edit": "Редактировать сцену", + "scenes.name": "Название:", + "scenes.name.hint": "Описательное имя для этого пресета сцены", + "scenes.name.placeholder": "Моя сцена", + "scenes.description": "Описание:", + "scenes.description.hint": "Необязательное описание назначения этой сцены", + "scenes.targets": "Цели:", + "scenes.targets.hint": "Выберите какие цели включить в снимок сцены", + "scenes.targets.add": "Добавить цель", + "scenes.targets.search_placeholder": "Поиск целей...", + "scenes.capture": "Захват", + "scenes.activate": "Активировать сцену", + "scenes.recapture": "Перезахватить текущее состояние", + "scenes.delete": "Удалить сцену", + "scenes.targets_count": "целей", + "scenes.captured": "Сцена захвачена", + "scenes.updated": "Сцена обновлена", + "scenes.activated": "Сцена активирована", + "scenes.activated_partial": "Сцена активирована частично", + "scenes.errors": "ошибок", + "scenes.recaptured": "Сцена перезахвачена", + "scenes.deleted": "Сцена удалена", + "scenes.recapture_confirm": "Перезахватить текущее состояние в \"{name}\"?", + "scenes.delete_confirm": "Удалить сцену \"{name}\"?", + "scenes.error.name_required": "Необходимо указать название", + "scenes.error.save_failed": "Не удалось сохранить сцену", + "scenes.error.activate_failed": "Не удалось активировать сцену", + "scenes.error.recapture_failed": "Не удалось перезахватить сцену", + "scenes.error.delete_failed": "Не удалось удалить сцену", + "scenes.cloned": "Сцена клонирована", + "scenes.error.clone_failed": "Не удалось клонировать сцену", + "time.hours_minutes": "{h}ч {m}м", + "time.minutes_seconds": "{m}м {s}с", + "time.seconds": "{s}с", + "dashboard.type.led": "LED", + "dashboard.type.kc": "Цвета клавиш", + "aria.close": "Закрыть", + "aria.save": "Сохранить", + "aria.cancel": "Отмена", + "aria.previous": "Назад", + "aria.next": "Вперёд", + "aria.hint": "Показать подсказку", + "color_strip.select_type": "Выберите тип цветовой полосы", + "color_strip.add": "Добавить", + "color_strip.edit": "Редактировать", + "color_strip.name": "Название:", + "color_strip.name.placeholder": "Настенная полоса", + "color_strip.picture_source": "Источник изображения:", + "color_strip.picture_source.hint": "Источник захвата экрана для расчёта цветов светодиодов", + "color_strip.fps": "Целевой FPS:", + "color_strip.fps.hint": "Целевая частота кадров для обновления цветов светодиодов (10-90)", + "color_strip.interpolation": "Режим цвета:", + "color_strip.interpolation.hint": "Как вычислять цвет светодиода по пикселям рамки", + "color_strip.interpolation.average": "Среднее", + "color_strip.interpolation.median": "Медиана", + "color_strip.interpolation.dominant": "Доминирующий", + "color_strip.interpolation.average.desc": "Смешивает все пиксели в усреднённый цвет", + "color_strip.interpolation.median.desc": "Берёт средний цвет, игнорируя выбросы", + "color_strip.interpolation.dominant.desc": "Использует самый частый цвет в выборке", + "color_strip.smoothing": "Сглаживание:", + "color_strip.smoothing.hint": "Временное смешивание кадров (0=без смешивания, 1=полное). Уменьшает мерцание.", + "color_strip.frame_interpolation": "Интерполяция кадров:", + "color_strip.frame_interpolation.hint": "Смешивает последовательные захваченные кадры для вывода на полной целевой частоте кадров, даже если скорость захвата ниже. Уменьшает заметные ступеньки при плавных переходах.", + "color_strip.color_corrections": "Цветокоррекция", + "color_strip.brightness": "Яркость:", + "color_strip.brightness.hint": "Множитель яркости (0=выкл, 1=без изменений, 2=двойная). Применяется после извлечения цвета.", + "color_strip.saturation": "Насыщенность:", + "color_strip.saturation.hint": "Насыщенность цвета (0=оттенки серого, 1=без изменений, 2=двойная насыщенность)", + "color_strip.gamma": "Гамма:", + "color_strip.gamma.hint": "Гамма-коррекция (1=без коррекции, <1=ярче средние тона, >1=темнее средние тона)", + "color_strip.test_device": "Тестировать на устройстве:", + "color_strip.test_device.hint": "Выберите устройство для отправки тестовых пикселей при нажатии на рамку", + "color_strip.leds": "Количество светодиодов", + "color_strip.led_count": "Количество LED:", + "color_strip.led_count.hint": "Общее число светодиодов на физической полосе. Для источников экрана: 0 = автоматически из калибровки (светодиоды за ТВ будут чёрными). Для статического цвета: укажите точное количество светодиодов устройства.", + "color_strip.created": "Источник цветовой полосы создан", + "color_strip.updated": "Источник цветовой полосы обновлён", + "color_strip.deleted": "Источник цветовой полосы удалён", + "color_strip.delete.confirm": "Удалить этот источник цветовой полосы?", + "color_strip.delete.referenced": "Невозможно удалить: источник используется в цели", + "color_strip.error.name_required": "Введите название", + "color_strip.type": "Тип:", + "color_strip.type.hint": "Источник изображения получает цвета светодиодов из захвата экрана. Статический цвет заполняет все светодиоды одним постоянным цветом. Градиент распределяет цветовой градиент по всем светодиодам. Смена цвета плавно циклически переключается между заданными цветами. Композит накладывает несколько источников как смешанные слои. Аудиореактив управляет LED от аудиосигнала в реальном времени. API-ввод принимает массивы цветов LED от внешних клиентов через REST или WebSocket.", + "color_strip.type.picture": "Источник изображения", + "color_strip.type.picture.desc": "Цвета из захвата экрана", + "color_strip.type.picture_advanced": "Мультимонитор", + "color_strip.type.picture_advanced.desc": "Калибровка линиями по нескольким мониторам", + "color_strip.type.static": "Статический цвет", + "color_strip.type.static.desc": "Заливка одним цветом", + "color_strip.type.gradient": "Градиент", + "color_strip.type.gradient.desc": "Плавный переход цветов по ленте", + "color_strip.type.color_cycle": "Смена цвета", + "color_strip.type.color_cycle.desc": "Циклическая смена списка цветов", + "color_strip.static_color": "Цвет:", + "color_strip.static_color.hint": "Статический цвет, который будет отправлен на все светодиоды полосы.", + "color_strip.gradient.preview": "Градиент:", + "color_strip.gradient.preview.hint": "Предпросмотр градиента. Нажмите на дорожку маркеров чтобы добавить остановку. Перетащите маркеры для изменения позиции.", + "color_strip.gradient.stops": "Цветовые остановки:", + "color_strip.gradient.stops.hint": "Каждая остановка задаёт цвет в относительной позиции (0.0 = начало, 1.0 = конец). Кнопка ↔ добавляет цвет справа для создания резкого перехода.", + "color_strip.gradient.stops_count": "остановок", + "color_strip.gradient.add_stop": "+ Добавить", + "color_strip.gradient.position": "Позиция (0.0–1.0)", + "color_strip.gradient.bidir.hint": "Добавить второй цвет справа от этой остановки для создания резкого перехода в градиенте.", + "color_strip.gradient.min_stops": "Градиент должен содержать не менее 2 остановок", + "color_strip.gradient.preset": "Пресет:", + "color_strip.gradient.preset.hint": "Загрузить готовую палитру градиента. Выбор пресета заменяет текущие остановки.", + "color_strip.gradient.preset.custom": "— Свой —", + "color_strip.gradient.preset.rainbow": "Радуга", + "color_strip.gradient.preset.sunset": "Закат", + "color_strip.gradient.preset.ocean": "Океан", + "color_strip.gradient.preset.forest": "Лес", + "color_strip.gradient.preset.fire": "Огонь", + "color_strip.gradient.preset.lava": "Лава", + "color_strip.gradient.preset.aurora": "Аврора", + "color_strip.gradient.preset.ice": "Лёд", + "color_strip.gradient.preset.warm": "Тёплый", + "color_strip.gradient.preset.cool": "Холодный", + "color_strip.gradient.preset.neon": "Неон", + "color_strip.gradient.preset.pastel": "Пастельный", + "color_strip.gradient.preset.save_button": "Сохранить как пресет…", + "color_strip.gradient.preset.save_prompt": "Введите название пресета:", + "color_strip.gradient.preset.saved": "Пресет сохранён", + "color_strip.gradient.preset.deleted": "Пресет удалён", + "color_strip.gradient.preset.apply": "Применить", + "color_strip.animation": "Анимация", + "color_strip.animation.type": "Эффект:", + "color_strip.animation.type.hint": "Эффект анимации.", + "color_strip.animation.type.none": "Нет (без эффекта анимации)", + "color_strip.animation.type.none.desc": "Статичные цвета без анимации", + "color_strip.animation.type.breathing": "Дыхание", + "color_strip.animation.type.breathing.desc": "Плавное угасание и нарастание яркости", + "color_strip.animation.type.color_cycle": "Смена цвета", + "color_strip.animation.type.gradient_shift": "Сдвиг градиента", + "color_strip.animation.type.gradient_shift.desc": "Сдвигает градиент вдоль ленты", + "color_strip.animation.type.wave": "Волна", + "color_strip.animation.type.wave.desc": "Синусоидальная волна яркости вдоль ленты", + "color_strip.animation.type.strobe": "Стробоскоп", + "color_strip.animation.type.strobe.desc": "Быстрое мигание вкл/выкл", + "color_strip.animation.type.sparkle": "Искры", + "color_strip.animation.type.sparkle.desc": "Случайные светодиоды кратковременно вспыхивают", + "color_strip.animation.type.pulse": "Пульс", + "color_strip.animation.type.pulse.desc": "Резкая вспышка яркости с быстрым затуханием", + "color_strip.animation.type.candle": "Свеча", + "color_strip.animation.type.candle.desc": "Тёплое мерцание, как у свечи", + "color_strip.animation.type.rainbow_fade": "Радужный перелив", + "color_strip.animation.type.rainbow_fade.desc": "Циклический переход по всему спектру оттенков", + "color_strip.animation.speed": "Скорость:", + "color_strip.animation.speed.hint": "Множитель скорости анимации. 1.0 ≈ один цикл в секунду для дыхания; большие значения ускоряют анимацию.", + "color_strip.color_cycle.colors": "Цвета:", + "color_strip.color_cycle.colors.hint": "Список цветов для плавной циклической смены. Минимум 2 цвета. По умолчанию — полный радужный спектр.", + "color_strip.color_cycle.add_color": "+ Добавить цвет", + "color_strip.color_cycle.speed": "Скорость:", + "color_strip.color_cycle.speed.hint": "Множитель скорости смены. 1.0 ≈ один полный цикл за 20 секунд; большие значения ускоряют смену.", + "color_strip.color_cycle.min_colors": "Смена цвета должна содержать не менее 2 цветов", + "color_strip.type.effect": "Эффект", + "color_strip.type.effect.desc": "Процедурные эффекты: огонь, плазма, аврора", + "color_strip.type.effect.hint": "Процедурные LED-эффекты (огонь, метеор, плазма, шум, аврора), генерируемые в реальном времени.", + "color_strip.type.composite": "Композит", + "color_strip.type.composite.desc": "Наложение и смешивание источников", + "color_strip.type.composite.hint": "Наложение нескольких источников цветовой ленты как слоёв с режимами смешивания и прозрачностью.", + "color_strip.type.mapped": "Маппинг", + "color_strip.type.mapped.desc": "Назначение источников на зоны LED", + "color_strip.type.mapped.hint": "Назначает разные источники цветовой полосы на разные диапазоны LED (зоны). В отличие от композита, маппинг размещает источники рядом друг с другом.", + "color_strip.type.audio": "Аудиореактив", + "color_strip.type.audio.desc": "LED от аудиосигнала", + "color_strip.type.audio.hint": "Цвета LED управляются аудиосигналом в реальном времени — системный звук или микрофон.", + "color_strip.type.api_input": "API-ввод", + "color_strip.type.api_input.desc": "Приём цветов от внешних приложений", + "color_strip.type.api_input.hint": "Принимает массивы цветов LED от внешних клиентов через REST POST или WebSocket. Используйте для интеграции с собственным ПО, домашней автоматизацией или любой системой, способной отправлять HTTP-запросы.", + "color_strip.api_input.fallback_color": "Цвет по умолчанию:", + "color_strip.api_input.fallback_color.hint": "Цвет для отображения, когда данные не получены в течение периода ожидания. LED покажут этот цвет при запуске и после потери соединения.", + "color_strip.api_input.timeout": "Тайм-аут (секунды):", + "color_strip.api_input.timeout.hint": "Время ожидания новых данных о цветах перед возвратом к цвету по умолчанию. Установите 0, чтобы не использовать тайм-аут.", + "color_strip.api_input.endpoints": "Эндпоинты для отправки:", + "color_strip.api_input.endpoints.hint": "Используйте эти URL для отправки данных о цветах LED из вашего внешнего приложения. REST принимает JSON, WebSocket принимает как JSON, так и бинарные кадры.", + "color_strip.api_input.save_first": "Сначала сохраните источник, чтобы увидеть URL эндпоинтов.", + "color_strip.api_input.interpolation": "Интерполяция LED:", + "color_strip.api_input.interpolation.hint": "Как масштабировать входящие данные LED, когда их количество отличается от количества LED на устройстве. Линейная — плавное смешивание, Ближайший — чёткие границы, Нет — обрезка или дополнение нулями.", + "color_strip.api_input.interpolation.linear": "Линейная", + "color_strip.api_input.interpolation.linear.desc": "Плавное смешивание между LED", + "color_strip.api_input.interpolation.nearest": "Ближайший", + "color_strip.api_input.interpolation.nearest.desc": "Чёткие границы, без смешивания", + "color_strip.api_input.interpolation.none": "Нет", + "color_strip.api_input.interpolation.none.desc": "Обрезка или дополнение нулями", + "color_strip.type.notification": "Уведомления", + "color_strip.type.notification.desc": "Разовый эффект по вебхуку", + "color_strip.type.notification.hint": "Вспышка, пульс или волна при срабатывании через вебхук. Предназначен для использования как слой в композитном источнике.", + "color_strip.notification.os_listener": "Слушать системные уведомления:", + "color_strip.notification.os_listener.hint": "Когда включено, источник автоматически срабатывает при появлении уведомления рабочего стола (Windows toast / Linux D-Bus). Требует разрешения на доступ к уведомлениям.", + "color_strip.notification.effect": "Эффект:", + "color_strip.notification.effect.hint": "Визуальный эффект при уведомлении. Вспышка — линейное затухание, Пульс — плавная волна, Волна — заполнение и затухание.", + "color_strip.notification.effect.flash": "Вспышка", + "color_strip.notification.effect.flash.desc": "Мгновенное включение, линейное затухание", + "color_strip.notification.effect.pulse": "Пульс", + "color_strip.notification.effect.pulse.desc": "Плавное свечение колоколом", + "color_strip.notification.effect.sweep": "Волна", + "color_strip.notification.effect.sweep.desc": "Заполняет слева направо, затем гаснет", + "color_strip.notification.duration": "Длительность (мс):", + "color_strip.notification.duration.hint": "Как долго длится эффект уведомления в миллисекундах.", + "color_strip.notification.default_color": "Цвет по умолчанию:", + "color_strip.notification.default_color.hint": "Цвет, когда для приложения нет специфического назначения цвета.", + "color_strip.notification.filter_mode": "Фильтр приложений:", + "color_strip.notification.filter_mode.hint": "Фильтр уведомлений по имени приложения. Выкл = все, Белый список = только указанные, Чёрный список = все кроме указанных.", + "color_strip.notification.filter_mode.off": "Выкл", + "color_strip.notification.filter_mode.whitelist": "Белый список", + "color_strip.notification.filter_mode.blacklist": "Чёрный список", + "color_strip.notification.filter_mode.off.desc": "Принимать все уведомления", + "color_strip.notification.filter_mode.whitelist.desc": "Только указанные приложения", + "color_strip.notification.filter_mode.blacklist.desc": "Все кроме указанных приложений", + "color_strip.notification.filter_list": "Список приложений:", + "color_strip.notification.filter_list.hint": "Одно имя приложения на строку. Используйте «Обзор» для выбора из запущенных процессов.", + "color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram", + "color_strip.notification.app_colors": "Цвета приложений", + "color_strip.notification.app_colors.label": "Назначения цветов:", + "color_strip.notification.app_colors.hint": "Индивидуальные цвета для приложений. Каждая строка связывает имя приложения с цветом уведомления.", + "color_strip.notification.app_colors.add": "+ Добавить", + "color_strip.notification.app_overrides": "Настройки приложений", + "color_strip.notification.app_overrides.label": "Переопределения:", + "color_strip.notification.app_overrides.hint": "Индивидуальные настройки цвета и звука для каждого приложения.", + "color_strip.notification.app_overrides.add": "+ Добавить", + "color_strip.notification.app_overrides.app_placeholder": "Имя приложения", + "color_strip.notification.sound": "Звук", + "color_strip.notification.sound.asset": "Звуковой ассет:", + "color_strip.notification.sound.asset.hint": "Выберите звуковой ассет для воспроизведения при уведомлении. Оставьте пустым для тишины.", + "color_strip.notification.sound.none": "Нет (без звука)", + "color_strip.notification.sound.search": "Поиск звуков…", + "color_strip.notification.sound.volume": "Громкость:", + "color_strip.notification.sound.volume.hint": "Общая громкость звуков уведомлений (0–100%).", + "color_strip.notification.sound.app_sounds": "Звуки приложений:", + "color_strip.notification.sound.app_sounds.hint": "Переопределение звука и громкости для конкретных приложений. Пустой звук = отключить для этого приложения.", + "color_strip.notification.sound.app_sounds.add": "+ Добавить", + "color_strip.notification.sound.app_name_placeholder": "Имя приложения", + "color_strip.notification.endpoint": "Вебхук:", + "color_strip.notification.endpoint.hint": "URL для запуска уведомлений из внешних систем. POST с JSON телом: {\"app\": \"AppName\", \"color\": \"#FF0000\"}.", + "color_strip.notification.save_first": "Сначала сохраните источник, чтобы увидеть URL вебхука.", + "color_strip.notification.app_count": "прилож.", + "color_strip.notification.test": "Тестовое уведомление", + "color_strip.notification.test.ok": "Уведомление отправлено", + "color_strip.notification.test.no_streams": "Нет запущенных потоков для этого источника", + "color_strip.notification.test.error": "Не удалось отправить уведомление", + "color_strip.notification.history.title": "История уведомлений", + "color_strip.notification.history.hint": "Последние ОС-уведомления, захваченные слушателем (новейшие сверху). До 50 записей.", + "color_strip.notification.history.empty": "Уведомления ещё не захвачены", + "color_strip.notification.history.unavailable": "Слушатель уведомлений ОС недоступен на этой платформе", + "color_strip.notification.history.error": "Не удалось загрузить историю уведомлений", + "color_strip.notification.history.refresh": "Обновить", + "color_strip.notification.history.unknown_app": "Неизвестное приложение", + "color_strip.notification.history.fired": "Потоков запущено", + "color_strip.notification.history.filtered": "Потоков отфильтровано", + "color_strip.test.title": "Предпросмотр", + "color_strip.test.connecting": "Подключение...", + "color_strip.test.error": "Не удалось подключиться к потоку предпросмотра", + "color_strip.test.led_count": "Кол-во LED:", + "color_strip.test.fps": "FPS:", + "color_strip.test.receive_fps": "Частота приёма", + "color_strip.test.apply": "Применить", + "color_strip.test.composite": "Композит", + "color_strip.preview.title": "Предпросмотр", + "color_strip.preview.not_connected": "Не подключено", + "color_strip.preview.connecting": "Подключение...", + "color_strip.preview.connected": "Подключено", + "color_strip.preview.unsupported": "Предпросмотр недоступен для этого типа источника", + "color_strip.type.daylight": "Дневной цикл", + "color_strip.type.daylight.desc": "Имитация естественного дневного света за 24 часа", + "color_strip.type.daylight.hint": "Имитирует цветовую температуру солнца в течение суток — от тёплого рассвета до прохладного дневного света, заката и ночи.", + "color_strip.daylight.speed": "Скорость:", + "color_strip.daylight.speed.hint": "Множитель скорости цикла. 1.0 = полный цикл день/ночь за ~4 минуты.", + "color_strip.daylight.use_real_time": "Реальное время:", + "color_strip.daylight.use_real_time.hint": "Если включено, цвет LED соответствует реальному времени суток. Настройка скорости игнорируется.", + "color_strip.daylight.real_time": "Реальное время", + "color_strip.daylight.latitude": "Широта:", + "color_strip.daylight.latitude.hint": "Географическая широта (-90 до 90). Влияет на время восхода/заката в режиме реального времени.", + "color_strip.type.candlelight": "Свечи", + "color_strip.type.candlelight.desc": "Реалистичная имитация мерцания свечей", + "color_strip.type.candlelight.hint": "Реалистичное мерцание свечей с тёплыми тонами и органическими паттернами.", + "color_strip.type.weather": "Погода", + "color_strip.type.weather.desc": "Погодно-реактивные амбиентные цвета", + "color_strip.type.weather.hint": "Отображает текущие погодные условия в амбиентные цвета LED. Требуется сущность Источник погоды.", + "color_strip.weather.source": "Источник погоды:", + "color_strip.weather.source.hint": "Источник метеоданных. Сначала создайте его во вкладке Погода.", + "color_strip.weather.speed": "Скорость анимации:", + "color_strip.weather.speed.hint": "Скорость дрейфа цветов. Выше = быстрее.", + "color_strip.weather.temperature_influence": "Влияние температуры:", + "color_strip.weather.temperature_influence.hint": "Насколько температура смещает палитру в тёплую/холодную сторону. 0 = чистые цвета условий, 1 = сильное смещение.", + "color_strip.weather.error.no_source": "Выберите источник погоды", + "color_strip.candlelight.color": "Базовый цвет:", + "color_strip.candlelight.color.hint": "Тёплый базовый цвет пламени свечи. По умолчанию — натуральный тёплый янтарь.", + "color_strip.candlelight.intensity": "Интенсивность мерцания:", + "color_strip.candlelight.intensity.hint": "Сила мерцания свечей. Низкие значения — мягкое свечение, высокие — свеча на ветру.", + "color_strip.candlelight.num_candles_label": "Количество свечей:", + "color_strip.candlelight.num_candles": "свечей", + "color_strip.candlelight.num_candles.hint": "Сколько независимых источников свечей вдоль ленты. Каждый мерцает по-своему.", + "color_strip.candlelight.speed": "Скорость мерцания:", + "color_strip.candlelight.speed.hint": "Скорость анимации мерцания. Большие значения — более быстрое, беспокойное пламя.", + "color_strip.type.processed": "Обработанный", + "color_strip.type.processed.desc": "Применить шаблон обработки к другому источнику", + "color_strip.type.processed.hint": "Оборачивает существующий источник цветовой полосы и пропускает его вывод через цепочку фильтров.", + "color_strip.processed.input": "Источник:", + "color_strip.processed.input.hint": "Источник цветовой полосы, вывод которого будет обработан", + "color_strip.processed.template": "Шаблон обработки:", + "color_strip.processed.template.hint": "Цепочка фильтров для применения к выводу входного источника", + "color_strip.processed.error.no_input": "Выберите входной источник", + "color_strip.composite.layers": "Слои:", + "color_strip.composite.layers.hint": "Наложение нескольких источников. Первый слой — нижний, последний — верхний. Каждый слой может иметь свой режим смешивания и прозрачность.", + "color_strip.composite.add_layer": "+ Добавить слой", + "color_strip.composite.source": "Источник", + "color_strip.composite.blend_mode": "Смешивание", + "color_strip.composite.blend_mode.normal": "Обычное", + "color_strip.composite.blend_mode.normal.desc": "Стандартное альфа-смешивание", + "color_strip.composite.blend_mode.add": "Сложение", + "color_strip.composite.blend_mode.add.desc": "Осветляет, складывая цвета", + "color_strip.composite.blend_mode.multiply": "Умножение", + "color_strip.composite.blend_mode.multiply.desc": "Затемняет, умножая цвета", + "color_strip.composite.blend_mode.screen": "Экран", + "color_strip.composite.blend_mode.screen.desc": "Осветляет, обратное умножение", + "color_strip.composite.blend_mode.override": "Замена", + "color_strip.composite.blend_mode.overlay": "Наложение", + "color_strip.composite.blend_mode.overlay.desc": "Умножение тёмных, осветление светлых", + "color_strip.composite.blend_mode.soft_light": "Мягкий свет", + "color_strip.composite.blend_mode.soft_light.desc": "Мягкая коррекция контраста", + "color_strip.composite.blend_mode.hard_light": "Жёсткий свет", + "color_strip.composite.blend_mode.hard_light.desc": "Сильный контраст, яркие цвета", + "color_strip.composite.blend_mode.difference": "Разница", + "color_strip.composite.blend_mode.difference.desc": "Абсолютная разница цветов", + "color_strip.composite.blend_mode.exclusion": "Исключение", + "color_strip.composite.blend_mode.exclusion.desc": "Как разница, но мягче", + "color_strip.composite.blend_mode.override.desc": "Чёрный = прозрачный, яркий = непрозрачный", + "color_strip.composite.opacity": "Непрозрачность", + "color_strip.composite.brightness": "Яркость", + "color_strip.composite.brightness.none": "Нет (полная яркость)", + "color_strip.composite.processing": "Обработка", + "color_strip.composite.enabled": "Включён", + "color_strip.composite.error.min_layers": "Необходим хотя бы 1 слой", + "color_strip.composite.error.no_source": "Для каждого слоя должен быть выбран источник", + "color_strip.composite.layers_count": "слоёв", + "color_strip.composite.range": "Диапазон LED", + "color_strip.composite.range_start": "Начало", + "color_strip.composite.range_end": "Конец", + "color_strip.composite.reverse": "Реверс", + "color_strip.mapped.zones": "Зоны:", + "color_strip.mapped.zones.hint": "Каждая зона привязывает источник цветовой полосы к определённому диапазону LED. Зоны размещаются рядом — промежутки остаются чёрными.", + "color_strip.mapped.add_zone": "+ Добавить зону", + "color_strip.mapped.zone_source": "Источник", + "color_strip.mapped.zone_start": "Начало LED", + "color_strip.mapped.zone_end": "Конец LED", + "color_strip.mapped.zone_reverse": "Реверс", + "color_strip.mapped.zones_count": "зон", + "color_strip.mapped.select_source": "Поиск источников...", + "color_strip.mapped.error.no_source": "Для каждой зоны должен быть выбран источник", + "color_strip.audio.visualization": "Визуализация:", + "color_strip.audio.visualization.hint": "Способ отображения аудиоданных на LED.", + "color_strip.audio.viz.spectrum": "Анализатор спектра", + "color_strip.audio.viz.spectrum.desc": "Частотные полосы по ленте", + "color_strip.audio.viz.beat_pulse": "Пульс бита", + "color_strip.audio.viz.beat_pulse.desc": "Все LED пульсируют в такт", + "color_strip.audio.viz.vu_meter": "VU-метр", + "color_strip.audio.viz.vu_meter.desc": "Уровень громкости заполняет ленту", + "color_strip.audio.source": "Аудиоисточник:", + "color_strip.audio.source.hint": "Аудиоисточник для визуализации. Может быть многоканальным (устройство) или моно (один канал). Создавайте и управляйте аудиоисточниками на вкладке Источники.", + "color_strip.audio.sensitivity": "Чувствительность:", + "color_strip.audio.sensitivity.hint": "Множитель усиления аудиосигнала. Более высокие значения делают LED чувствительнее к тихим звукам.", + "color_strip.audio.smoothing": "Сглаживание:", + "color_strip.audio.smoothing.hint": "Временное сглаживание между кадрами. Более высокие значения дают плавную, но медленнее реагирующую визуализацию.", + "color_strip.audio.palette": "Палитра:", + "color_strip.audio.palette.hint": "Цветовая палитра для полос спектра или пульсации бита.", + "color_strip.audio.color": "Базовый цвет:", + "color_strip.audio.color.hint": "Цвет низкого уровня для полосы VU-метра.", + "color_strip.audio.color_peak": "Пиковый цвет:", + "color_strip.audio.color_peak.hint": "Цвет высокого уровня в верхней части полосы VU-метра.", + "color_strip.audio.mirror": "Зеркало:", + "color_strip.audio.mirror.hint": "Зеркалирование спектра от центра к краям: басы в середине, высокие частоты по краям.", + "color_strip.effect.type": "Тип эффекта:", + "color_strip.effect.type.hint": "Выберите процедурный алгоритм.", + "color_strip.effect.fire": "Огонь", + "color_strip.effect.fire.desc": "Клеточный автомат, имитирующий поднимающееся пламя с диффузией тепла", + "color_strip.effect.meteor": "Метеор", + "color_strip.effect.meteor.desc": "Яркая точка движется по ленте с экспоненциально затухающим хвостом", + "color_strip.effect.plasma": "Плазма", + "color_strip.effect.plasma.desc": "Наложение синусоидальных волн с палитрой — классический демо-эффект", + "color_strip.effect.noise": "Шум", + "color_strip.effect.noise.desc": "Прокручиваемый фрактальный шум, отображённый на палитру", + "color_strip.effect.aurora": "Аврора", + "color_strip.effect.aurora.desc": "Наложенные шумовые полосы, дрейфующие и смешивающиеся — в стиле северного сияния", + "color_strip.effect.speed": "Скорость:", + "color_strip.effect.speed.hint": "Множитель скорости анимации эффекта (0.1 = очень медленно, 10.0 = очень быстро).", + "color_strip.effect.palette": "Палитра:", + "color_strip.effect.palette.hint": "Цветовая палитра для отображения значений эффекта в RGB-цвета.", + "color_strip.effect.color": "Цвет метеора:", + "color_strip.effect.color.hint": "Цвет головной точки метеора.", + "color_strip.effect.intensity": "Интенсивность:", + "color_strip.effect.intensity.hint": "Интенсивность эффекта — частота искр (огонь), затухание хвоста (метеор) или диапазон яркости (аврора).", + "color_strip.effect.scale": "Масштаб:", + "color_strip.effect.scale.hint": "Пространственный масштаб — частота волн (плазма), уровень масштабирования (шум) или ширина полос (аврора).", + "color_strip.effect.mirror": "Отражение:", + "color_strip.effect.mirror.hint": "Режим отскока — метеор меняет направление у краёв ленты вместо переноса.", + "color_strip.palette.fire": "Огонь", + "color_strip.palette.ocean": "Океан", + "color_strip.palette.lava": "Лава", + "color_strip.palette.forest": "Лес", + "color_strip.palette.rainbow": "Радуга", + "color_strip.palette.aurora": "Аврора", + "color_strip.palette.sunset": "Закат", + "color_strip.palette.ice": "Лёд", + "audio_source.title": "Аудиоисточники", + "audio_source.group.multichannel": "Многоканальные", + "audio_source.group.mono": "Моно", + "audio_source.group.band_extract": "Полосовой фильтр", + "audio_source.add": "Добавить аудиоисточник", + "audio_source.add.multichannel": "Добавить многоканальный", + "audio_source.add.mono": "Добавить моно", + "audio_source.add.band_extract": "Добавить полосовой фильтр", + "audio_source.edit": "Редактировать аудиоисточник", + "audio_source.edit.multichannel": "Редактировать многоканальный", + "audio_source.edit.mono": "Редактировать моно", + "audio_source.edit.band_extract": "Редактировать полосовой фильтр", + "audio_source.name": "Название:", + "audio_source.name.placeholder": "Системный звук", + "audio_source.name.hint": "Описательное имя для этого аудиоисточника", + "audio_source.type": "Тип:", + "audio_source.type.hint": "Многоканальный захватывает все каналы с аудиоустройства. Моно извлекает один канал из многоканального источника.", + "audio_source.type.multichannel": "Многоканальный", + "audio_source.type.mono": "Моно", + "audio_source.device": "Аудиоустройство:", + "audio_source.device.hint": "Источник аудиосигнала. Устройства обратной петли захватывают системный звук; устройства ввода — микрофон или линейный вход.", + "audio_source.refresh_devices": "Обновить устройства", + "audio_source.parent": "Родительский источник:", + "audio_source.parent.hint": "Многоканальный источник для извлечения канала", + "audio_source.channel": "Канал:", + "audio_source.channel.hint": "Какой аудиоканал извлечь из многоканального источника", + "audio_source.channel.mono": "Моно (Л+П микс)", + "audio_source.channel.left": "Левый", + "audio_source.channel.right": "Правый", + "audio_source.description": "Описание (необязательно):", + "audio_source.description.placeholder": "Опишите этот аудиоисточник...", + "audio_source.description.hint": "Необязательные заметки об этом аудиоисточнике", + "audio_source.created": "Аудиоисточник создан", + "audio_source.updated": "Аудиоисточник обновлён", + "audio_source.deleted": "Аудиоисточник удалён", + "audio_source.delete.confirm": "Удалить этот аудиоисточник?", + "audio_source.error.name_required": "Введите название", + "audio_source.audio_template": "Аудиошаблон:", + "audio_source.audio_template.hint": "Шаблон аудиозахвата определяет, какой движок и настройки использовать для этого устройства", + "audio_source.band_parent": "Родительский аудиоисточник:", + "audio_source.band_parent.hint": "Аудиоисточник для извлечения частотной полосы", + "audio_source.band": "Частотная полоса:", + "audio_source.band.hint": "Выберите предустановку частотной полосы или произвольный диапазон", + "audio_source.band.bass": "Басы (20–250 Гц)", + "audio_source.band.mid": "Средние (250–4000 Гц)", + "audio_source.band.treble": "Высокие (4000–20000 Гц)", + "audio_source.band.custom": "Произвольный диапазон", + "audio_source.freq_low": "Нижняя частота (Гц):", + "audio_source.freq_high": "Верхняя частота (Гц):", + "audio_source.freq_range": "Частотный диапазон", + "audio_source.test": "Тест", + "audio_source.test.title": "Тест аудиоисточника", + "audio_source.test.rms": "RMS", + "audio_source.test.peak": "Пик", + "audio_source.test.beat": "Бит", + "audio_source.test.connecting": "Подключение...", + "audio_source.test.error": "Ошибка теста аудио", + "audio_template.test": "Тест", + "audio_template.test.title": "Тест аудиошаблона", + "audio_template.test.device": "Аудиоустройство:", + "audio_template.test.device.hint": "Выберите устройство для захвата звука во время теста", + "audio_template.test.run": "Запуск", + "audio_template.title": "Аудиошаблоны", + "audio_template.add": "Добавить аудиошаблон", + "audio_template.edit": "Редактировать аудиошаблон", + "audio_template.name": "Название шаблона:", + "audio_template.name.placeholder": "Мой аудиошаблон", + "audio_template.description.label": "Описание (необязательно):", + "audio_template.description.placeholder": "Опишите этот шаблон...", + "audio_template.engine": "Аудиодвижок:", + "audio_template.engine.hint": "Выберите движок аудиозахвата. WASAPI — только Windows с поддержкой loopback. Sounddevice — кроссплатформенный.", + "audio_template.engine.unavailable": "Недоступен", + "audio_template.engine.unavailable.hint": "Этот движок недоступен в вашей системе", + "audio_template.config": "Конфигурация", + "audio_template.config.show": "Показать конфигурацию", + "audio_template.created": "Аудиошаблон создан", + "audio_template.updated": "Аудиошаблон обновлён", + "audio_template.deleted": "Аудиошаблон удалён", + "audio_template.delete.confirm": "Удалить этот аудиошаблон?", + "audio_template.error.load": "Не удалось загрузить аудиошаблоны", + "audio_template.error.engines": "Не удалось загрузить аудиодвижки", + "audio_template.error.required": "Пожалуйста, заполните все обязательные поля", + "audio_template.error.delete": "Не удалось удалить аудиошаблон", + "streams.group.value": "Источники значений", + "streams.group.sync": "Часы синхронизации", + "tree.group.picture": "Источники изображений", + "tree.group.capture": "Захват экрана", + "tree.group.static": "Статичные", + "tree.group.processing": "Обработанные", + "tree.group.strip": "Цветовые полосы", + "tree.group.audio": "Аудио", + "tree.group.utility": "Утилиты", + "tree.leaf.sources": "Источники", + "tree.leaf.engine_templates": "Шаблоны движка", + "tree.leaf.images": "Изображения", + "tree.leaf.video": "Видео", + "tree.leaf.filter_templates": "Шаблоны фильтров", + "tree.leaf.processing_templates": "Шаблоны обработки", + "tree.leaf.templates": "Шаблоны", + "value_source.group.title": "Источники значений", + "value_source.select_type": "Выберите тип источника значений", + "value_source.add": "Добавить источник значений", + "value_source.edit": "Редактировать источник значений", + "value_source.name": "Название:", + "value_source.name.placeholder": "Пульс яркости", + "value_source.name.hint": "Описательное имя для этого источника значений", + "value_source.type": "Тип:", + "value_source.type.hint": "Статический выдаёт постоянное значение. Анимированный циклически меняет форму волны. Аудио реагирует на звук. Адаптивные типы автоматически подстраивают яркость по времени суток или содержимому сцены.", + "value_source.type.static": "Статический", + "value_source.type.static.desc": "Постоянное выходное значение", + "value_source.type.animated": "Анимированный", + "value_source.type.animated.desc": "Циклическая смена по форме волны", + "value_source.type.audio": "Аудио", + "value_source.type.audio.desc": "Реагирует на звуковой сигнал", + "value_source.type.adaptive_time": "Адаптивный (Время)", + "value_source.type.adaptive_time.desc": "Подстройка по времени суток", + "value_source.type.adaptive_scene": "Адаптивный (Сцена)", + "value_source.type.adaptive_scene.desc": "Подстройка по содержимому сцены", + "value_source.type.daylight": "Дневной цикл", + "value_source.type.daylight.desc": "Яркость следует за циклом дня/ночи", + "value_source.daylight.speed": "Скорость:", + "value_source.daylight.speed.hint": "Множитель скорости цикла. 1.0 = полный цикл день/ночь за ~4 минуты.", + "value_source.daylight.use_real_time": "Реальное время:", + "value_source.daylight.use_real_time.hint": "Яркость следует за реальным временем суток. Скорость игнорируется.", + "value_source.daylight.enable_real_time": "Следовать за часами", + "value_source.daylight.latitude": "Широта:", + "value_source.daylight.latitude.hint": "Географическая широта (-90 до 90). Влияет на время восхода/заката в режиме реального времени.", + "value_source.daylight.real_time": "Реальное время", + "value_source.daylight.speed_label": "Скорость", + "value_source.value": "Значение:", + "value_source.value.hint": "Постоянное выходное значение (0.0 = выкл, 1.0 = полная яркость)", + "value_source.waveform": "Форма волны:", + "value_source.waveform.hint": "Форма цикла анимации яркости", + "value_source.waveform.sine": "Синус", + "value_source.waveform.triangle": "Треугольник", + "value_source.waveform.square": "Прямоугольник", + "value_source.waveform.sawtooth": "Пила", + "value_source.speed": "Скорость (цикл/мин):", + "value_source.speed.hint": "Циклов в минуту — как быстро повторяется волна (1 = очень медленно, 120 = очень быстро)", + "value_source.min_value": "Мин. значение:", + "value_source.min_value.hint": "Минимальный выход цикла волны", + "value_source.max_value": "Макс. значение:", + "value_source.max_value.hint": "Максимальный выход цикла волны", + "value_source.audio_source": "Аудиоисточник:", + "value_source.audio_source.hint": "Аудиоисточник для считывания уровня звука (многоканальный или моно)", + "value_source.mode": "Режим:", + "value_source.mode.hint": "RMS измеряет среднюю громкость. Пик отслеживает самые громкие моменты. Бит реагирует на ритм.", + "value_source.mode.rms": "RMS (Громкость)", + "value_source.mode.peak": "Пик", + "value_source.mode.beat": "Бит", + "value_source.mode.rms.desc": "Средний уровень громкости", + "value_source.mode.peak.desc": "Отслеживание пиковых моментов", + "value_source.mode.beat.desc": "Детекция ритмических ударов", + "value_source.auto_gain": "Авто-усиление:", + "value_source.auto_gain.hint": "Автоматически нормализует уровни звука, чтобы выходное значение использовало полный диапазон независимо от громкости входного сигнала", + "value_source.auto_gain.enable": "Включить авто-усиление", + "value_source.sensitivity": "Чувствительность:", + "value_source.sensitivity.hint": "Множитель усиления аудиосигнала (выше = более реактивный)", + "value_source.scene_sensitivity.hint": "Множитель усиления сигнала яркости (выше = более чувствительный к изменениям яркости)", + "value_source.smoothing": "Сглаживание:", + "value_source.smoothing.hint": "Временное сглаживание (0 = мгновенный отклик, 1 = очень плавный/медленный)", + "value_source.audio_min_value": "Мин. значение:", + "value_source.audio_min_value.hint": "Выход при тишине (напр. 0.3 = минимум 30% яркости)", + "value_source.audio_max_value": "Макс. значение:", + "value_source.audio_max_value.hint": "Выход при максимальном уровне звука", + "value_source.schedule": "Расписание:", + "value_source.schedule.hint": "Определите минимум 2 временные точки. Яркость линейно интерполируется между ними, с переходом через полночь.", + "value_source.schedule.add": "+ Добавить точку", + "value_source.schedule.points": "точек", + "value_source.picture_source": "Источник изображения:", + "value_source.picture_source.hint": "Источник изображения, кадры которого будут анализироваться на среднюю яркость.", + "value_source.scene_behavior": "Поведение:", + "value_source.scene_behavior.hint": "Дополнение: тёмная сцена = высокая яркость (для фоновой подсветки). Совпадение: яркая сцена = высокая яркость.", + "value_source.scene_behavior.complement": "Дополнение (тёмный → ярко)", + "value_source.scene_behavior.match": "Совпадение (яркий → ярко)", + "value_source.adaptive_min_value": "Мин. значение:", + "value_source.adaptive_min_value.hint": "Минимальная выходная яркость", + "value_source.adaptive_max_value": "Макс. значение:", + "value_source.adaptive_max_value.hint": "Максимальная выходная яркость", + "value_source.error.schedule_min": "Расписание требует минимум 2 временные точки", + "value_source.description": "Описание (необязательно):", + "value_source.description.placeholder": "Опишите этот источник значений...", + "value_source.description.hint": "Необязательные заметки об этом источнике значений", + "value_source.created": "Источник значений создан", + "value_source.updated": "Источник значений обновлён", + "value_source.deleted": "Источник значений удалён", + "value_source.delete.confirm": "Удалить этот источник значений?", + "value_source.error.name_required": "Введите название", + "value_source.test": "Тест", + "value_source.test.title": "Тест источника значений", + "value_source.test.connecting": "Подключение...", + "value_source.test.error": "Не удалось подключиться", + "value_source.test.current": "Текущее", + "value_source.test.min": "Мин", + "value_source.test.max": "Макс", + "test.frames": "Кадры", + "test.fps": "Кадр/с", + "test.avg_capture": "Сред", + "targets.brightness_vs": "Источник яркости:", + "targets.brightness_vs.hint": "Необязательный источник значений для динамического управления яркостью каждый кадр (переопределяет яркость устройства)", + "targets.brightness_vs.none": "Нет (яркость устройства)", + "targets.min_brightness_threshold": "Мин. порог яркости:", + "targets.min_brightness_threshold.hint": "Если итоговая яркость (яркость пикселей × яркость устройства/источника) ниже этого значения, светодиоды полностью выключаются (0 = отключено)", + "targets.adaptive_fps": "Адаптивный FPS:", + "targets.adaptive_fps.hint": "Автоматически снижает частоту отправки, когда устройство перестаёт отвечать, и постепенно восстанавливает её при стабилизации. Рекомендуется для WiFi-устройств со слабым сигналом.", + "targets.protocol": "Протокол:", + "targets.protocol.hint": "DDP отправляет пиксели по быстрому UDP (рекомендуется). HTTP использует JSON API — медленнее, но надёжнее, ограничение ~500 LED.", + "targets.protocol.ddp": "DDP (UDP)", + "targets.protocol.ddp.desc": "Быстрые UDP-пакеты — рекомендуется", + "targets.protocol.http": "HTTP", + "targets.protocol.http.desc": "JSON API — медленнее, ≤500 LED", + "targets.protocol.serial": "Serial", + "search.open": "Поиск (Ctrl+K)", + "search.placeholder": "Поиск... (Ctrl+K)", + "search.loading": "Загрузка...", + "search.no_results": "Ничего не найдено", + "search.group.devices": "Устройства", + "search.group.targets": "LED-цели", + "search.group.kc_targets": "Цели Key Colors", + "search.group.css": "Источники цветных лент", + "search.group.automations": "Автоматизации", + "search.group.streams": "Потоки изображений", + "search.group.capture_templates": "Шаблоны захвата", + "search.group.pp_templates": "Шаблоны постобработки", + "search.group.pattern_templates": "Шаблоны паттернов", + "search.group.audio": "Аудиоисточники", + "search.group.value": "Источники значений", + "search.group.scenes": "Пресеты сцен", + "search.group.cspt": "Шаблоны обработки полос", + "search.group.sync_clocks": "Синхронные часы", + "search.group.actions": "Действия", + "search.action.start": "Запустить", + "search.action.stop": "Остановить", + "search.action.activate": "Активировать", + "search.action.enable": "Включить", + "search.action.disable": "Отключить", + "settings.backup.label": "Резервное копирование", + "settings.backup.hint": "Скачать всю конфигурацию (устройства, цели, потоки, шаблоны, автоматизации) в виде одного JSON-файла.", + "settings.backup.button": "Скачать резервную копию", + "settings.backup.success": "Резервная копия скачана", + "settings.backup.error": "Ошибка скачивания резервной копии", + "settings.restore.label": "Восстановление конфигурации", + "settings.restore.hint": "Загрузите ранее сохранённый файл резервной копии для замены всей конфигурации. Сервер перезапустится автоматически.", + "settings.restore.button": "Восстановить из копии", + "settings.restore.confirm": "Это заменит ВСЮ конфигурацию и перезапустит сервер. Вы уверены?", + "settings.restore.success": "Конфигурация восстановлена", + "settings.restore.error": "Ошибка восстановления", + "settings.restore.restarting": "Сервер перезапускается...", + "settings.restore.restart_timeout": "Сервер не отвечает. Обновите страницу вручную.", + "settings.restart_server": "Перезапустить сервер", + "settings.restart_confirm": "Перезапустить сервер? Активные цели будут остановлены.", + "settings.restarting": "Перезапуск сервера...", + "settings.button.close": "Закрыть", + "settings.log_level.label": "Уровень логирования", + "settings.log_level.hint": "Изменить подробность логов сервера в реальном времени. DEBUG — максимум деталей, CRITICAL — только критические ошибки.", + "settings.log_level.save": "Применить", + "settings.log_level.saved": "Уровень логирования изменён", + "settings.log_level.save_error": "Не удалось изменить уровень логирования", + "settings.log_level.desc.debug": "Подробный вывод для разработки", + "settings.log_level.desc.info": "Обычные сообщения", + "settings.log_level.desc.warning": "Возможные проблемы", + "settings.log_level.desc.error": "Только ошибки", + "settings.log_level.desc.critical": "Только критические ошибки", + "settings.auto_backup.label": "Авто-бэкап", + "settings.auto_backup.hint": "Автоматическое создание периодических резервных копий конфигурации. Старые копии удаляются при превышении максимального количества.", + "settings.auto_backup.enable": "Включить авто-бэкап", + "settings.auto_backup.interval_label": "Интервал", + "settings.auto_backup.max_label": "Макс. копий", + "settings.auto_backup.save": "Сохранить настройки", + "settings.auto_backup.saved": "Настройки авто-бэкапа сохранены", + "settings.auto_backup.save_error": "Не удалось сохранить настройки авто-бэкапа", + "settings.auto_backup.backup_now": "Создать бэкап", + "settings.auto_backup.backup_created": "Бэкап создан", + "settings.auto_backup.backup_error": "Ошибка создания бэкапа", + "settings.auto_backup.last_backup": "Последний бэкап", + "settings.auto_backup.never": "Никогда", + "settings.saved_backups.label": "Сохранённые копии", + "settings.saved_backups.hint": "Файлы авто-бэкапа на сервере. Скачайте для локального хранения или удалите для освобождения места.", + "settings.saved_backups.empty": "Нет сохранённых копий", + "settings.saved_backups.restore": "Восстановить", + "settings.saved_backups.download": "Скачать", + "settings.saved_backups.delete": "Удалить", + "settings.saved_backups.delete_confirm": "Удалить эту резервную копию?", + "settings.saved_backups.delete_error": "Не удалось удалить копию", + "settings.saved_backups.type.auto": "авто", + "settings.saved_backups.type.manual": "ручной", + "settings.mqtt.label": "MQTT", + "settings.mqtt.hint": "Настройте подключение к MQTT-брокеру для условий и триггеров автоматизации.", + "settings.mqtt.enabled": "Включить MQTT", + "settings.mqtt.host_label": "Хост брокера", + "settings.mqtt.port_label": "Порт", + "settings.mqtt.username_label": "Имя пользователя", + "settings.mqtt.password_label": "Пароль", + "settings.mqtt.password_set_hint": "Пароль задан — оставьте пустым, чтобы сохранить", + "settings.mqtt.client_id_label": "Идентификатор клиента", + "settings.mqtt.base_topic_label": "Базовый топик", + "settings.mqtt.save": "Сохранить настройки MQTT", + "settings.mqtt.saved": "Настройки MQTT сохранены", + "settings.mqtt.save_error": "Не удалось сохранить настройки MQTT", + "settings.mqtt.error_host_required": "Требуется указать хост брокера", + "settings.logs.label": "Журнал сервера", + "settings.logs.hint": "Просмотр журнала сервера в реальном времени. Используйте фильтр для отображения нужных уровней.", + "settings.logs.connect": "Подключить", + "settings.logs.disconnect": "Отключить", + "settings.logs.clear": "Очистить", + "settings.logs.error": "Ошибка подключения к журналу", + "settings.logs.filter.all": "Все уровни", + "settings.logs.filter.info": "Info+", + "settings.logs.filter.warning": "Warning+", + "settings.logs.filter.error": "Только ошибки", + "settings.logs.filter.all_desc": "Все сообщения лога", + "settings.logs.filter.info_desc": "Info, предупреждения и ошибки", + "settings.logs.filter.warning_desc": "Только предупреждения и ошибки", + "settings.logs.filter.error_desc": "Только ошибки", + "device.error.power_off_failed": "Не удалось выключить устройство", + "device.error.remove_failed": "Не удалось удалить устройство", + "device.error.settings_load_failed": "Не удалось загрузить настройки устройства", + "device.error.brightness": "Не удалось обновить яркость", + "device.error.required": "Пожалуйста, заполните все поля", + "device.error.update": "Не удалось обновить устройство", + "device.error.save": "Не удалось сохранить настройки", + "device.error.clone_failed": "Не удалось клонировать устройство", + "device_discovery.error.fill_all_fields": "Пожалуйста, заполните все поля", + "device_discovery.added": "Устройство успешно добавлено", + "device_discovery.error.add_failed": "Не удалось добавить устройство", + "calibration.error.load_failed": "Не удалось загрузить калибровку", + "calibration.error.css_load_failed": "Не удалось загрузить источник цветовой полосы", + "calibration.error.test_toggle_failed": "Не удалось переключить тестовый край", + "calibration.error.save_failed": "Не удалось сохранить калибровку", + "calibration.error.led_count_mismatch": "Общее количество LED должно совпадать с количеством LED устройства", + "calibration.error.led_count_exceeded": "Калиброванных LED больше, чем общее количество LED", + "calibration.mode.simple": "Простой", + "calibration.mode.advanced": "Расширенный", + "calibration.switch_to_advanced": "Расширенный режим", + "calibration.advanced.title": "Расширенная калибровка", + "calibration.advanced.switch_to_simple": "Простой режим", + "calibration.advanced.lines_title": "Линии", + "calibration.advanced.canvas_hint": "Перетаскивайте мониторы. Нажимайте на грани для выбора линий. Прокрутка — масштаб, перетаскивание пустого места — сдвиг.", + "calibration.advanced.reset_view": "Сбросить вид", + "calibration.advanced.line_properties": "Свойства линии", + "calibration.advanced.picture_source": "Источник:", + "calibration.advanced.picture_source.hint": "Источник изображения (монитор), с которого эта линия снимает данные", + "calibration.advanced.edge": "Грань:", + "calibration.advanced.edge.hint": "С какой грани экрана снимать пиксели", + "calibration.advanced.led_count": "Светодиоды:", + "calibration.advanced.led_count.hint": "Количество светодиодов на этой линии", + "calibration.advanced.span_start": "Начало:", + "calibration.advanced.span_start.hint": "Откуда начинается захват вдоль грани (0 = начало, 1 = конец). Позволяет покрыть только часть грани.", + "calibration.advanced.span_end": "Конец:", + "calibration.advanced.span_end.hint": "Где заканчивается захват вдоль грани (0 = начало, 1 = конец). Вместе с «Начало» определяет активный участок.", + "calibration.advanced.border_width": "Глубина (пкс):", + "calibration.advanced.border_width.hint": "Сколько пикселей вглубь от края захватывать. Большие значения берут больше внутренней части экрана.", + "calibration.advanced.reverse": "Реверс", + "calibration.advanced.no_lines_warning": "Добавьте хотя бы одну линию", + "dashboard.error.automation_toggle_failed": "Не удалось переключить автоматизацию", + "dashboard.error.start_failed": "Не удалось запустить обработку", + "dashboard.error.stop_failed": "Не удалось остановить обработку", + "dashboard.error.stop_all": "Не удалось остановить все цели", + "target.error.editor_open_failed": "Не удалось открыть редактор цели", + "target.error.start_failed": "Не удалось запустить цель", + "target.error.stop_failed": "Не удалось остановить цель", + "target.error.clone_failed": "Не удалось клонировать цель", + "target.error.delete_failed": "Не удалось удалить цель", + "targets.stop_all.button": "Остановить все", + "targets.stop_all.none_running": "Нет запущенных целей", + "targets.stop_all.stopped": "Остановлено целей: {count}", + "targets.stop_all.error": "Не удалось остановить цели", + "audio_source.error.load": "Не удалось загрузить аудиоисточник", + "audio_template.error.clone_failed": "Не удалось клонировать аудиошаблон", + "value_source.error.load": "Не удалось загрузить источник значений", + "color_strip.error.editor_open_failed": "Не удалось открыть редактор цветовой полосы", + "color_strip.error.clone_failed": "Не удалось клонировать источник цветовой полосы", + "color_strip.error.delete_failed": "Не удалось удалить источник цветовой полосы", + "pattern.error.editor_open_failed": "Не удалось открыть редактор шаблона узоров", + "pattern.error.clone_failed": "Не удалось клонировать шаблон узоров", + "pattern.error.delete_failed": "Не удалось удалить шаблон узоров", + "pattern.error.capture_bg_failed": "Не удалось захватить фон", + "stream.error.clone_picture_failed": "Не удалось клонировать источник изображения", + "stream.error.clone_capture_failed": "Не удалось клонировать шаблон захвата", + "stream.error.clone_pp_failed": "Не удалось клонировать шаблон постобработки", + "theme.switched.dark": "Переключено на тёмную тему", + "theme.switched.light": "Переключено на светлую тему", + "accent.color.updated": "Цвет акцента обновлён", + "search.footer": "↑↓ навигация · Enter выбор · Esc закрыть", + "sync_clock.group.title": "Часы синхронизации", + "sync_clock.add": "Добавить часы", + "sync_clock.edit": "Редактировать часы", + "sync_clock.name": "Название:", + "sync_clock.name.placeholder": "Основные часы анимации", + "sync_clock.name.hint": "Описательное название для этих часов синхронизации", + "sync_clock.speed": "Скорость:", + "sync_clock.speed.hint": "Множитель скорости анимации для всех привязанных источников. 1.0 = обычная, 2.0 = двойная, 0.5 = половинная.", + "sync_clock.description": "Описание (необязательно):", + "sync_clock.description.placeholder": "Необязательное описание", + "sync_clock.description.hint": "Необязательные заметки о назначении этих часов", + "sync_clock.status.running": "Работает", + "sync_clock.status.paused": "Приостановлено", + "sync_clock.action.pause": "Приостановить", + "sync_clock.action.resume": "Возобновить", + "sync_clock.action.reset": "Сбросить", + "sync_clock.error.name_required": "Название часов обязательно", + "sync_clock.error.load": "Не удалось загрузить часы синхронизации", + "sync_clock.created": "Часы синхронизации созданы", + "sync_clock.updated": "Часы синхронизации обновлены", + "sync_clock.deleted": "Часы синхронизации удалены", + "sync_clock.paused": "Часы приостановлены", + "sync_clock.resumed": "Часы возобновлены", + "sync_clock.reset_done": "Часы сброшены на ноль", + "sync_clock.delete.confirm": "Удалить эти часы синхронизации? Привязанные источники потеряют синхронизацию и будут работать на скорости по умолчанию.", + "sync_clock.elapsed": "Прошло времени", + "weather_source.group.title": "Источники погоды", + "weather_source.add": "Добавить источник погоды", + "weather_source.edit": "Редактировать источник погоды", + "weather_source.name": "Название:", + "weather_source.name.placeholder": "Моя погода", + "weather_source.name.hint": "Описательное название источника погоды", + "weather_source.provider": "Провайдер:", + "weather_source.provider.hint": "Провайдер метеоданных. Open-Meteo бесплатный и не требует API ключа.", + "weather_source.provider.open_meteo.desc": "Бесплатно, без API ключа", + "weather_source.location": "Местоположение:", + "weather_source.location.hint": "Географические координаты. Используйте автоопределение или введите вручную.", + "weather_source.latitude": "Шир:", + "weather_source.longitude": "Долг:", + "weather_source.use_my_location": "Определить", + "weather_source.update_interval": "Интервал обновления:", + "weather_source.update_interval.hint": "Частота запроса метеоданных. Меньше = более оперативные обновления.", + "weather_source.description": "Описание (необязательно):", + "weather_source.description.placeholder": "Необязательное описание", + "weather_source.test": "Тест", + "weather_source.error.name_required": "Название источника погоды обязательно", + "weather_source.error.load": "Не удалось загрузить источник погоды", + "weather_source.created": "Источник погоды создан", + "weather_source.updated": "Источник погоды обновлён", + "weather_source.deleted": "Источник погоды удалён", + "weather_source.delete.confirm": "Удалить этот источник погоды? Связанные источники цветовых лент потеряют данные о погоде.", + "weather_source.geo.success": "Местоположение определено", + "weather_source.geo.error": "Ошибка геолокации", + "weather_source.geo.not_supported": "Геолокация не поддерживается вашим браузером", + "streams.group.weather": "Погода", + "section.empty.weather_sources": "Нет источников погоды. Нажмите + для добавления.", + "color_strip.clock": "Часы синхронизации:", + "color_strip.clock.hint": "Привязка к часам для синхронизации анимации между источниками. Скорость управляется на часах.", + "graph.title": "Граф", + "graph.fit_all": "Показать все узлы", + "graph.zoom_in": "Приблизить", + "graph.zoom_out": "Отдалить", + "graph.search": "Поиск узлов", + "graph.search_placeholder": "Поиск сущностей...", + "graph.legend": "Легенда", + "graph.minimap": "Миникарта", + "graph.relayout": "Перестроить", + "graph.empty": "Ещё нет сущностей", + "graph.empty.hint": "Создайте устройства, источники и цели, чтобы увидеть их здесь.", + "graph.disconnect": "Отключить", + "graph.connection_updated": "Соединение обновлено", + "graph.connection_failed": "Не удалось обновить соединение", + "graph.connection_removed": "Соединение удалено", + "graph.disconnect_failed": "Не удалось отключить", + "graph.relayout_confirm": "Сбросить все ручные позиции узлов и перестроить граф?", + "graph.fullscreen": "Полноэкранный режим", + "graph.add_entity": "Добавить сущность", + "graph.color_picker": "Цвет узла", + "graph.filter": "Фильтр узлов", + "graph.filter_placeholder": "Фильтр по имени...", + "graph.filter_clear": "Очистить фильтр", + "graph.filter_running": "Запущен", + "graph.filter_stopped": "Остановлен", + "graph.filter_types": "Типы", + "graph.filter_group.capture": "Захват", + "graph.filter_group.strip": "Цвет. полосы", + "graph.filter_group.audio": "Аудио", + "graph.filter_group.targets": "Цели", + "graph.filter_group.other": "Другое", + "graph.bulk_delete_confirm": "Удалить {count} выбранных сущностей?", + "graph.nothing_to_undo": "Нечего отменять", + "graph.nothing_to_redo": "Нечего повторять", + "graph.help_title": "Горячие клавиши", + "graph.help.search": "Поиск", + "graph.help.filter": "Фильтр", + "graph.help.add": "Добавить сущность", + "graph.help.shortcuts": "Горячие клавиши", + "graph.help.delete": "Удалить / Отсоединить", + "graph.help.select_all": "Выбрать все", + "graph.help.undo": "Отменить", + "graph.help.redo": "Повторить", + "graph.help.fullscreen": "Полный экран", + "graph.help.deselect": "Снять выбор", + "graph.help.navigate": "Навигация по узлам", + "graph.help.click": "Клик", + "graph.help.click_desc": "Выбрать узел", + "graph.help.dblclick": "Двойной клик", + "graph.help.dblclick_desc": "Приблизить к узлу", + "graph.help.shift_click": "Shift+Клик", + "graph.help.shift_click_desc": "Множественный выбор", + "graph.help.shift_drag": "Shift+Перетащить", + "graph.help.shift_drag_desc": "Выбор рамкой", + "graph.help.drag_node": "Перетащить узел", + "graph.help.drag_node_desc": "Переместить", + "graph.help.drag_port": "Перетащить порт", + "graph.help.drag_port_desc": "Соединить сущности", + "graph.help.right_click": "ПКМ по связи", + "graph.help.right_click_desc": "Отсоединить связь", + "graph.tooltip.fps": "FPS", + "graph.tooltip.errors": "Ошибки", + "graph.tooltip.uptime": "Время работы", + "automation.enabled": "Автоматизация включена", + "automation.disabled": "Автоматизация выключена", + "scene_preset.activated": "Пресет активирован", + "scene_preset.used_by": "Используется в %d автоматизации(ях)", + "settings.api_keys.label": "API-ключи", + "settings.api_keys.hint": "API-ключи определяются в конфигурационном файле сервера (config.yaml). Отредактируйте файл и перезапустите сервер для применения изменений.", + "settings.api_keys.empty": "API-ключи не настроены", + "settings.api_keys.load_error": "Не удалось загрузить API-ключи", + "settings.partial.label": "Частичный экспорт / импорт", + "settings.partial.hint": "Экспортировать или импортировать один тип объектов. Импорт заменяет или объединяет данные и перезапускает сервер.", + "settings.partial.store.devices": "Устройства", + "settings.partial.store.output_targets": "LED-цели", + "settings.partial.store.color_strip_sources": "Цветные полосы", + "settings.partial.store.picture_sources": "Источники изображений", + "settings.partial.store.audio_sources": "Аудио-источники", + "settings.partial.store.audio_templates": "Аудио-шаблоны", + "settings.partial.store.capture_templates": "Шаблоны захвата", + "settings.partial.store.postprocessing_templates": "Шаблоны постобработки", + "settings.partial.store.color_strip_processing_templates": "Шаблоны обработки полос", + "settings.partial.store.pattern_templates": "Шаблоны паттернов", + "settings.partial.store.value_sources": "Источники значений", + "settings.partial.store.sync_clocks": "Синхронные часы", + "settings.partial.store.automations": "Автоматизации", + "settings.partial.store.scene_presets": "Пресеты сцен", + "settings.partial.export_button": "Экспорт", + "settings.partial.import_button": "Импорт из файла", + "settings.partial.merge_label": "Объединить (добавить/перезаписать, сохранить существующие)", + "settings.partial.export_success": "Экспорт выполнен", + "settings.partial.export_error": "Ошибка экспорта", + "settings.partial.import_success": "Импорт выполнен", + "settings.partial.import_error": "Ошибка импорта", + "settings.partial.import_confirm_replace": "Это ЗАМЕНИТ все данные {store} и перезапустит сервер. Продолжить?", + "settings.partial.import_confirm_merge": "Это ОБЪЕДИНИТ данные {store} и перезапустит сервер. Продолжить?", + "section.empty.devices": "Устройств пока нет. Нажмите + для добавления.", + "section.empty.targets": "LED-целей пока нет. Нажмите + для добавления.", + "section.empty.kc_targets": "Целей ключевых цветов пока нет. Нажмите + для добавления.", + "section.empty.pattern_templates": "Шаблонов паттернов пока нет. Нажмите + для добавления.", + "section.empty.picture_sources": "Источников пока нет. Нажмите + для добавления.", + "section.empty.capture_templates": "Шаблонов захвата пока нет. Нажмите + для добавления.", + "section.empty.pp_templates": "Шаблонов постобработки пока нет. Нажмите + для добавления.", + "section.empty.audio_sources": "Аудио-источников пока нет. Нажмите + для добавления.", + "section.empty.audio_templates": "Аудио-шаблонов пока нет. Нажмите + для добавления.", + "section.empty.color_strips": "Цветных полос пока нет. Нажмите + для добавления.", + "section.empty.value_sources": "Источников значений пока нет. Нажмите + для добавления.", + "section.empty.sync_clocks": "Синхронных часов пока нет. Нажмите + для добавления.", + "section.empty.cspt": "Шаблонов обработки полос пока нет. Нажмите + для добавления.", + "section.empty.automations": "Автоматизаций пока нет. Нажмите + для добавления.", + "section.empty.scenes": "Пресетов сцен пока нет. Нажмите + для добавления.", + "bulk.select": "Выбрать", + "bulk.cancel": "Отмена", + "bulk.selected_count.one": "{count} выбран", + "bulk.selected_count.few": "{count} выбрано", + "bulk.selected_count.many": "{count} выбрано", + "bulk.select_all": "Выбрать все", + "bulk.deselect_all": "Снять выбор", + "bulk.delete": "Удалить", + "bulk.start": "Запустить", + "bulk.stop": "Остановить", + "bulk.enable": "Включить", + "bulk.disable": "Выключить", + "bulk.confirm_delete.one": "Удалить {count} элемент?", + "bulk.confirm_delete.few": "Удалить {count} элемента?", + "bulk.confirm_delete.many": "Удалить {count} элементов?", + "appearance.style.label": "Стили оформления", + "appearance.style.hint": "Выберите визуальную тему — шрифт и цветовая палитра применяются вместе.", + "appearance.preset.default": "Стандарт", + "appearance.preset.midnight": "Полночь", + "appearance.preset.ember": "Угли", + "appearance.preset.arctic": "Арктика", + "appearance.preset.terminal": "Терминал", + "appearance.preset.neon": "Неон", + "appearance.preset.sakura": "Сакура", + "appearance.preset.ocean": "Океан", + "appearance.preset.copper": "Медь", + "appearance.preset.vapor": "Вейпорвейв", + "appearance.preset.monolith": "Монолит", + "appearance.preset.applied": "Стиль применён", + "appearance.bg.label": "Фоновые эффекты", + "appearance.bg.hint": "Добавьте фоновый слой за интерфейсом.", + "appearance.bg.none": "Нет", + "appearance.bg.noise": "Шумовое поле", + "appearance.bg.aurora": "Северное сияние", + "appearance.bg.plasma": "Плазма", + "appearance.bg.rain": "Цифровой дождь", + "appearance.bg.stars": "Звёздное поле", + "appearance.bg.warp": "Тоннель", + "appearance.bg.grid": "Точечная сетка", + "appearance.bg.mesh": "Градиент", + "appearance.bg.scanlines": "Развёртка", + "appearance.bg.applied": "Фоновый эффект применён", + "settings.tab.updates": "Обновления", + "settings.tab.about": "О программе", + "update.status_label": "Статус обновления", + "update.current_version": "Текущая версия:", + "update.badge_tooltip": "Доступна новая версия — нажмите для подробностей", + "update.available": "Доступна версия {version}", + "update.up_to_date": "Установлена последняя версия", + "update.prerelease": "пре-релиз", + "update.view_release": "Подробнее", + "update.dismiss": "Скрыть", + "update.check_now": "Проверить обновления", + "update.check_error": "Ошибка проверки обновлений", + "update.last_check": "Последняя проверка", + "update.never": "никогда", + "update.release_notes": "Примечания к релизу", + "update.view_release_notes": "Открыть примечания к релизу", + "update.auto_check_label": "Автоматическая проверка", + "update.auto_check_hint": "Периодически проверять наличие новых версий в фоновом режиме.", + "update.enable": "Включить автопроверку", + "update.interval_label": "Интервал проверки", + "update.channel_label": "Канал", + "update.channel.stable": "Стабильный", + "update.channel.stable_desc": "Только стабильные релизы", + "update.channel.prerelease": "Пре-релиз", + "update.channel.prerelease_desc": "Включая альфа, бета и RC сборки", + "update.save_settings": "Сохранить настройки", + "update.settings_saved": "Настройки обновлений сохранены", + "update.settings_save_error": "Не удалось сохранить настройки обновлений", + "update.apply_now": "Обновить сейчас", + "update.apply_confirm": "Скачать и установить версию {version}? Сервер будет перезапущен автоматически.", + "update.apply_error": "Ошибка обновления", + "update.applying": "Применяется обновление…", + "update.downloading": "Загрузка…", + "update.install_type_label": "Тип установки:", + "update.install_type.installer": "Установщик Windows", + "update.install_type.portable": "Портативная", + "update.install_type.docker": "Docker", + "update.install_type.dev": "Разработка", + "color_strip.notification.search_apps": "Поиск приложений…", + "asset.group.title": "Ресурсы", + "asset.upload": "Загрузить ресурс", + "asset.edit": "Редактировать ресурс", + "asset.name": "Название:", + "asset.name.hint": "Отображаемое название ресурса.", + "asset.description": "Описание:", + "asset.description.hint": "Необязательное описание ресурса.", + "asset.file": "Файл:", + "asset.file.hint": "Выберите файл для загрузки (звук, изображение, видео или другое).", + "asset.drop_or_browse": "Перетащите файл сюда или нажмите для выбора", + "asset.uploaded": "Ресурс загружен", + "asset.updated": "Ресурс обновлён", + "asset.deleted": "Ресурс удалён", + "asset.confirm_delete": "Удалить этот ресурс?", + "asset.error.name_required": "Название обязательно", + "asset.error.no_file": "Выберите файл для загрузки", + "asset.error.delete_failed": "Не удалось удалить ресурс", + "asset.error.play_failed": "Не удалось воспроизвести звук", + "asset.error.download_failed": "Не удалось скачать ресурс", + "asset.play": "Воспроизвести", + "asset.download": "Скачать", + "asset.prebuilt": "Встроенный", + "asset.prebuilt_restored": "Восстановлено встроенных ресурсов: {count}", + "asset.prebuilt_none_to_restore": "Все встроенные ресурсы уже доступны", + "asset.restore_prebuilt": "Восстановить встроенные звуки", + "asset.type.sound": "Звук", + "asset.type.image": "Изображение", + "asset.type.video": "Видео", + "asset.type.other": "Другое", + "streams.group.assets": "Ресурсы", + "section.empty.assets": "Ресурсов пока нет. Нажмите +, чтобы загрузить.", + "donation.message": "LedGrab — бесплатный проект с открытым кодом. Если он вам полезен, поддержите разработку.", + "donation.support": "Поддержать проект", + "donation.view_source": "Исходный код", + "donation.later": "Напомнить позже", + "donation.dismiss": "Больше не показывать", + "donation.about_title": "О LedGrab", + "donation.about_opensource": "LedGrab — программа с открытым исходным кодом, бесплатная для использования и модификации.", + "donation.about_donate": "Поддержать разработку", + "donation.about_license": "Лицензия MIT" +} diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index 91c000c..87705c9 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -1,1954 +1,1911 @@ { - "app.title": "LED Grab", - "app.version": "版本:", - "app.api_docs": "API 文档", - "app.connection_lost": "服务器不可达", - "app.connection_retrying": "正在尝试重新连接…", - "app.server_restarting": "服务器正在重启…", - "app.server_restarting_sub": "请稍候,服务器即将恢复。", - "demo.badge": "演示", - "demo.banner": "您正处于演示模式 — 所有设备和数据均为虚拟。未使用任何真实硬件。", - "theme.toggle": "切换主题", - "bg.anim.toggle": "切换动态背景", - "accent.title": "主题色", - "accent.custom": "自定义", - "accent.reset": "重置", - "locale.change": "切换语言", - "auth.login": "登录", - "auth.logout": "退出", - "auth.authenticated": "● 已认证", - "auth.title": "登录 LED Grab", - "auth.message": "请输入 API 密钥以进行身份验证并访问 LED Grab。", - "auth.label": "API 密钥:", - "auth.placeholder": "输入您的 API 密钥...", - "auth.hint": "API 密钥将安全存储在浏览器的本地存储中。", - "auth.button.cancel": "取消", - "auth.button.login": "登录", - "auth.error.required": "请输入 API 密钥", - "auth.success": "登录成功!", - "auth.logout.confirm": "确定要退出登录吗?", - "auth.logout.success": "已成功退出", - "auth.please_login": "请先登录", - "auth.session_expired": "会话已过期或 API 密钥无效,请重新登录。", - "auth.toggle_password": "切换密码可见性", - "auth.prompt_enter": "Enter your API key:", - "auth.prompt_update": "Current API key is set. Enter new key to update or leave blank to remove:", - "api_key.login": "登录", - "displays.title": "可用显示器", - "displays.layout": "显示器", - "displays.information": "显示器信息", - "displays.legend.primary": "主显示器", - "displays.legend.secondary": "副显示器", - "displays.badge.primary": "主", - "displays.badge.secondary": "副", - "displays.resolution": "分辨率:", - "displays.refresh_rate": "刷新率:", - "displays.position": "位置:", - "displays.index": "显示器序号:", - "displays.loading": "正在加载显示器...", - "displays.none": "没有可用的显示器", - "displays.failed": "加载显示器失败", - "displays.picker.title": "选择显示器", - "displays.picker.title.device": "选择设备", - "displays.picker.select": "选择显示器...", - "displays.picker.click_to_select": "点击选择此显示器", - "displays.picker.adb_connect": "连接 ADB 设备", - "displays.picker.adb_connect.placeholder": "IP 地址(例如 192.168.2.201)", - "displays.picker.adb_connect.button": "连接", - "displays.picker.adb_connect.success": "设备已连接", - "displays.picker.adb_connect.error": "连接设备失败", - "displays.picker.adb_disconnect": "断开连接", - "displays.picker.no_android": "未找到 Android 设备。请通过 USB 连接或在上方输入 IP 地址。", - "templates.title": "引擎模板", - "templates.description": "采集模板定义屏幕的采集方式。每个模板使用特定的采集引擎(MSS、DXcam、WGC)及自定义设置。将模板分配给设备以获得最佳性能。", - "templates.loading": "正在加载模板...", - "templates.empty": "尚未配置采集模板", - "templates.add": "添加引擎模板", - "templates.edit": "编辑引擎模板", - "templates.name": "模板名称:", - "templates.name.placeholder": "我的自定义模板", - "templates.description.label": "描述(可选):", - "templates.description.placeholder": "描述此模板...", - "templates.engine": "采集引擎:", - "templates.engine.hint": "选择要使用的屏幕采集技术", - "templates.engine.select": "选择引擎...", - "templates.engine.unavailable": "不可用", - "templates.engine.unavailable.hint": "此引擎在您的系统上不可用", - "templates.engine.mss.desc": "跨平台,纯Python", - "templates.engine.dxcam.desc": "DirectX,低延迟", - "templates.engine.bettercam.desc": "DirectX,高性能", - "templates.engine.camera.desc": "USB/IP摄像头捕获", - "templates.engine.scrcpy.desc": "Android屏幕镜像", - "templates.engine.wgc.desc": "Windows图形捕获", - "templates.config": "配置", - "templates.config.show": "显示配置", - "templates.config.none": "无额外配置", - "templates.config.default": "默认", - "templates.config.camera_backend.auto": "自动检测最佳后端", - "templates.config.camera_backend.dshow": "Windows DirectShow", - "templates.config.camera_backend.msmf": "Windows Media Foundation", - "templates.config.camera_backend.v4l2": "Linux Video4Linux2", - "templates.created": "模板创建成功", - "templates.updated": "模板更新成功", - "templates.deleted": "模板删除成功", - "templates.delete.confirm": "确定要删除此模板吗?", - "templates.error.load": "加载模板失败", - "templates.error.engines": "加载引擎失败", - "templates.error.required": "请填写所有必填项", - "templates.error.delete": "删除模板失败", - "templates.test.title": "测试采集", - "templates.test.description": "保存前测试此模板,查看采集预览和性能指标。", - "templates.test.display": "显示器:", - "templates.test.display.select": "选择显示器...", - "templates.test.duration": "采集时长(秒):", - "templates.test.border_width": "边框宽度(像素):", - "templates.test.run": "运行", - "templates.test.running": "正在运行测试...", - "templates.test.results.preview": "全幅采集预览", - "templates.test.results.borders": "边框提取", - "templates.test.results.top": "上", - "templates.test.results.right": "右", - "templates.test.results.bottom": "下", - "templates.test.results.left": "左", - "templates.test.results.performance": "性能", - "templates.test.results.capture_time": "采集", - "templates.test.results.extraction_time": "提取", - "templates.test.results.total_time": "总计", - "templates.test.results.max_fps": "最大 FPS", - "templates.test.results.duration": "时长", - "templates.test.results.frame_count": "帧数", - "templates.test.results.actual_fps": "实际 FPS", - "templates.test.results.avg_capture_time": "平均采集", - "templates.test.results.resolution": "分辨率:", - "templates.test.error.no_engine": "请选择采集引擎", - "templates.test.error.no_display": "请选择显示器", - "templates.test.error.failed": "测试失败", - "devices.title": "设备", - "device.select_type": "选择设备类型", - "devices.add": "添加新设备", - "devices.loading": "正在加载设备...", - "devices.none": "尚未配置设备", - "devices.failed": "加载设备失败", - "devices.wled_config": "WLED 配置:", - "devices.wled_note": "使用以下方式配置您的 WLED 设备(效果、分段、颜色顺序、功率限制等):", - "devices.wled_link": "官方 WLED 应用", - "devices.wled_note_or": "或内置的", - "devices.wled_webui_link": "WLED Web UI", - "devices.wled_note_webui": "(在浏览器中打开设备 IP 地址)。", - "devices.wled_note2": "此控制器发送像素颜色数据并控制每个设备的亮度。", - "device.scan": "自动发现", - "device.scan.empty": "未找到设备", - "device.scan.error": "网络扫描失败", - "device.scan.already_added": "已添加", - "device.scan.selected": "设备已选择", - "device.type": "设备类型:", - "device.type.hint": "选择 LED 控制器的类型", - "device.type.wled": "WLED", - "device.type.wled.desc": "通过HTTP/UDP控制的WiFi LED", - "device.type.adalight": "Adalight", - "device.type.adalight.desc": "Arduino串口LED协议", - "device.type.ambiled": "AmbiLED", - "device.type.ambiled.desc": "AmbiLED串口协议", - "device.type.mqtt": "MQTT", - "device.type.mqtt.desc": "通过MQTT代理发布LED数据", - "device.type.ws": "WebSocket", - "device.type.ws.desc": "通过WebSocket流式传输LED数据", - "device.type.openrgb": "OpenRGB", - "device.type.openrgb.desc": "通过OpenRGB控制RGB外设", - "device.type.dmx": "DMX", - "device.type.dmx.desc": "Art-Net / sACN (E1.31) 舞台灯光", - "device.type.mock": "Mock", - "device.type.mock.desc": "用于测试的虚拟设备", - "device.type.espnow": "ESP-NOW", - "device.type.espnow.desc": "Ultra-low-latency via ESP32 gateway", - "device.type.hue": "Philips Hue", - "device.type.hue.desc": "Hue Entertainment API streaming", - "device.type.usbhid": "USB HID", - "device.type.usbhid.desc": "USB RGB peripherals (keyboards, mice)", - "device.type.spi": "SPI Direct", - "device.type.spi.desc": "Raspberry Pi GPIO/SPI LED strips", - "device.type.chroma": "Razer Chroma", - "device.type.chroma.desc": "Razer peripherals via Chroma SDK", - "device.type.gamesense": "SteelSeries", - "device.type.gamesense.desc": "SteelSeries peripherals via GameSense", - "device.chroma.device_type": "Peripheral Type:", - "device.chroma.device_type.hint": "Which Razer peripheral to control via Chroma SDK", - "device.gamesense.device_type": "Peripheral Type:", - "device.gamesense.device_type.hint": "Which SteelSeries peripheral to control via GameSense", - "device.espnow.peer_mac": "Peer MAC:", - "device.espnow.peer_mac.hint": "MAC address of the remote ESP32 receiver (e.g. AA:BB:CC:DD:EE:FF)", - "device.espnow.channel": "WiFi Channel:", - "device.espnow.channel.hint": "WiFi channel (1-14). Must match the receiver's channel.", - "device.hue.url": "Bridge IP:", - "device.hue.url.hint": "IP address of your Hue bridge", - "device.hue.username": "Bridge Username:", - "device.hue.username.hint": "Hue bridge application key from pairing", - "device.hue.client_key": "Client Key:", - "device.hue.client_key.hint": "Entertainment API client key (hex string from pairing)", - "device.hue.group_id": "Entertainment Group:", - "device.hue.group_id.hint": "Entertainment configuration ID from your Hue bridge", - "device.usbhid.url": "VID:PID:", - "device.usbhid.url.hint": "USB Vendor:Product ID in hex (e.g. 1532:0084)", - "device.spi.url": "GPIO/SPI Path:", - "device.spi.url.hint": "GPIO pin or SPI device path (e.g. spi://gpio:18)", - "device.spi.speed": "SPI Speed (Hz):", - "device.spi.speed.hint": "SPI clock speed. 800000 Hz for WS2812, 2400000 Hz for APA102.", - "device.spi.led_type": "LED Chipset:", - "device.spi.led_type.hint": "Type of addressable LED strip connected to the GPIO/SPI pin", - "device.spi.led_type.ws2812b.desc": "Most common, 800 KHz data, 3-wire RGB", - "device.spi.led_type.ws2812.desc": "Original WS2812, 800 KHz, 3-wire RGB", - "device.spi.led_type.ws2811.desc": "External driver IC, 400 KHz, 12V strips", - "device.spi.led_type.sk6812.desc": "Samsung LED, 800 KHz, 3-wire RGB", - "device.spi.led_type.sk6812_rgbw.desc": "SK6812 with dedicated white channel", - "device.gamesense.peripheral.keyboard": "Keyboard", - "device.gamesense.peripheral.keyboard.desc": "Per-key RGB illumination", - "device.gamesense.peripheral.mouse": "Mouse", - "device.gamesense.peripheral.mouse.desc": "Mouse RGB zones", - "device.gamesense.peripheral.headset": "Headset", - "device.gamesense.peripheral.headset.desc": "Headset earcup lighting", - "device.gamesense.peripheral.mousepad": "Mousepad", - "device.gamesense.peripheral.mousepad.desc": "Mousepad edge lighting zones", - "device.gamesense.peripheral.indicator": "Indicator", - "device.gamesense.peripheral.indicator.desc": "OLED/LED status indicator", - "device.dmx_protocol": "DMX 协议:", - "device.dmx_protocol.hint": "Art-Net 使用 UDP 端口 6454,sACN (E1.31) 使用 UDP 端口 5568", - "device.dmx_protocol.artnet.desc": "UDP 单播,端口 6454", - "device.dmx_protocol.sacn.desc": "组播/单播,端口 5568", - "device.dmx_start_universe": "起始 Universe:", - "device.dmx_start_universe.hint": "第一个 DMX universe (0-32767)。超过 170 个 LED 时自动使用多个 universe。", - "device.dmx_start_channel": "起始通道:", - "device.dmx_start_channel.hint": "universe 中的第一个 DMX 通道 (1-512)", - "device.dmx.url": "IP 地址:", - "device.dmx.url.hint": "DMX 节点的 IP 地址(例如 192.168.1.50)", - "device.dmx.url.placeholder": "192.168.1.50", - "device.serial_port": "串口:", - "device.serial_port.hint": "选择 Adalight 设备的 COM 端口", - "device.serial_port.none": "未找到串口", - "device.serial_port.select": "选择端口...", - "device.led_count_manual.hint": "灯带上的 LED 数量(必须与 Arduino 程序匹配)", - "device.baud_rate": "波特率:", - "device.baud_rate.hint": "串口通信速率。越高 FPS 越高,但需要与 Arduino 程序匹配。", - "device.led_type": "LED 类型:", - "device.led_type.hint": "RGB(3通道)或 RGBW(4通道,带独立白色)", - "device.send_latency": "发送延迟(毫秒):", - "device.send_latency.hint": "每帧模拟网络/串口延迟(毫秒)", - "device.css_processing_template": "色带处理模板:", - "device.css_processing_template.hint": "应用于此设备所有色带输出的默认处理模板", - "device.mqtt_topic": "MQTT 主题:", - "device.mqtt_topic.hint": "用于发布像素数据的 MQTT 主题路径(例如 mqtt://ledgrab/device/name)", - "device.mqtt_topic.placeholder": "mqtt://ledgrab/device/客厅", - "device.ws_url": "连接 URL:", - "device.ws_url.hint": "客户端连接并接收 LED 数据的 WebSocket URL", - "device.openrgb.url": "OpenRGB URL:", - "device.openrgb.url.hint": "OpenRGB 服务器地址(例如 openrgb://localhost:6742/0)", - "device.openrgb.zone": "区域:", - "device.openrgb.zone.hint": "选择要控制的 LED 区域(全部不选则控制所有区域)", - "device.openrgb.zone.loading": "加载区域中…", - "device.openrgb.zone.error": "加载区域失败", - "device.openrgb.mode": "区域模式:", - "device.openrgb.mode.hint": "合并模式将所有区域作为一条连续 LED 灯带。独立模式让每个区域独立渲染完整效果。", - "device.openrgb.mode.combined": "合并灯带", - "device.openrgb.mode.separate": "独立区域", - "device.openrgb.added_multiple": "已添加 {count} 个设备", - "device.url.hint": "设备的 IP 地址或主机名(例如 http://192.168.1.100)", - "device.name": "设备名称:", - "device.name.placeholder": "客厅电视", - "device.url": "地址:", - "device.url.placeholder": "http://192.168.1.100", - "device.led_count": "LED 数量:", - "device.led_count.hint": "设备中配置的 LED 数量", - "device.led_count.hint.auto": "从设备自动检测", - "device.button.add": "添加设备", - "device.button.start": "启动", - "device.button.stop": "停止", - "device.button.settings": "常规设置", - "device.button.capture_settings": "采集设置", - "device.button.calibrate": "校准", - "device.button.remove": "移除", - "device.button.webui": "打开设备 Web UI", - "device.button.power_off": "关闭", - "device.button.ping": "Ping 设备", - "device.ping.online": "在线 ({ms}ms)", - "device.ping.offline": "设备离线", - "device.ping.error": "Ping 失败", - "device.power.off_success": "设备已关闭", - "device.status.connected": "已连接", - "device.status.disconnected": "已断开", - "device.status.error": "错误", - "device.status.processing": "处理中", - "device.status.idle": "空闲", - "device.fps": "FPS:", - "device.display": "显示器:", - "device.remove.confirm": "确定要移除此设备吗?", - "device.added": "设备添加成功", - "device.removed": "设备已移除", - "device.started": "处理已启动", - "device.stopped": "处理已停止", - "device.metrics.actual_fps": "实际 FPS", - "device.metrics.current_fps": "当前 FPS", - "device.metrics.target_fps": "目标 FPS", - "device.metrics.potential_fps": "潜在 FPS", - "device.metrics.frames": "帧数", - "device.metrics.frames_skipped": "已跳过", - "device.metrics.keepalive": "心跳", - "device.metrics.errors": "错误", - "device.metrics.uptime": "运行时长", - "device.metrics.timing": "管线时序:", - "device.metrics.device_fps": "设备刷新率", - "device.health.online": "在线", - "device.health.offline": "离线", - "device.health.streaming_unreachable": "流传输期间不可达", - "device.health.checking": "检测中...", - "device.last_seen.label": "最近检测", - "device.last_seen.just_now": "刚刚", - "device.last_seen.seconds": "%d秒前", - "device.last_seen.minutes": "%d分钟前", - "device.last_seen.hours": "%d小时前", - "device.last_seen.days": "%d天前", - "device.tutorial.start": "开始教程", - "device.tip.metadata": "设备信息(LED 数量、类型、颜色通道)从设备自动检测", - "device.tip.brightness": "滑动调节设备亮度", - "device.tip.start": "启动或停止屏幕采集处理", - "device.tip.settings": "配置设备常规设置(名称、地址、健康检查)", - "device.tip.capture_settings": "配置采集设置(显示器、采集模板)", - "device.tip.calibrate": "校准 LED 位置、方向和覆盖范围", - "device.tip.webui": "打开设备内置的 Web 界面进行高级配置", - "device.tip.add": "点击此处添加新的 LED 设备", - "settings.title": "设置", - "settings.tab.general": "常规", - "settings.tab.backup": "备份", - "settings.tab.mqtt": "MQTT", - "settings.tab.appearance": "外观", - "settings.logs.open_viewer": "打开日志查看器", - "settings.external_url.label": "外部 URL", - "settings.external_url.hint": "设置后,此基础 URL 将用于 webhook 链接和其他用户可见的链接,代替自动检测的本地 IP。示例:https://myserver.example.com:8080", - "settings.external_url.placeholder": "https://myserver.example.com:8080", - "settings.external_url.save": "保存", - "settings.external_url.saved": "外部 URL 已保存", - "settings.external_url.save_error": "保存外部 URL 失败", - "settings.general.title": "常规设置", - "settings.capture.title": "采集设置", - "settings.capture.saved": "采集设置已更新", - "settings.capture.failed": "保存采集设置失败", - "settings.brightness": "亮度:", - "settings.brightness.hint": "此设备的全局亮度(0-100%)", - "settings.url.hint": "设备的 IP 地址或主机名", - "settings.display_index": "显示器:", - "settings.display_index.hint": "为此设备采集哪个屏幕", - "settings.fps": "目标 FPS:", - "settings.fps.hint": "目标帧率(10-90)", - "settings.capture_template": "引擎模板:", - "settings.capture_template.hint": "此设备的屏幕采集引擎和配置", - "settings.button.cancel": "取消", - "settings.health_interval": "健康检查间隔(秒):", - "settings.health_interval.hint": "检查设备状态的频率(5-600秒)", - "settings.auto_shutdown": "自动恢复:", - "settings.auto_shutdown.hint": "当目标停止或服务器关闭时恢复设备到空闲状态", - "settings.button.save": "保存更改", - "settings.saved": "设置保存成功", - "settings.failed": "保存设置失败", - "calibration.title": "LED 校准", - "calibration.tip.led_count": "输入每条边的 LED 数量", - "calibration.tip.start_corner": "点击角落设置起始位置", - "calibration.tip.direction": "切换灯带方向(顺时针/逆时针)", - "calibration.tip.offset": "设置 LED 偏移 — 从 LED 0 到起始角落的距离", - "calibration.tip.span": "拖动绿色条调整覆盖范围", - "calibration.tip.test": "点击边缘切换测试 LED", - "calibration.tip.overlay": "切换屏幕叠加层以查看显示器上的 LED 位置和编号", - "calibration.tip.toggle_inputs": "点击 LED 总数切换边缘输入", - "calibration.tip.border_width": "从屏幕边缘采样多少像素来确定 LED 颜色", - "calibration.tip.skip_leds_start": "跳过灯带起始端的 LED — 被跳过的 LED 保持关闭", - "calibration.tip.skip_leds_end": "跳过灯带末尾端的 LED — 被跳过的 LED 保持关闭", - "tour.welcome": "欢迎使用 LED Grab!快速导览将带您了解界面。使用方向键或按钮进行导航。", - "tour.dashboard": "仪表盘 — 实时查看运行中的目标、自动化和设备状态。", - "tour.targets": "目标 — 添加 WLED 设备,配置 LED 目标的捕获设置和校准。", - "tour.sources": "来源 — 管理捕获模板、图片来源、音频来源和色带。", - "tour.graph": "图表 — 所有实体及其连接的可视化概览。拖动端口进行连接,右键单击边线断开连接。", - "tour.automations": "自动化 — 通过时间、音频或数值条件自动切换场景。", - "tour.settings": "设置 — 备份和恢复配置,管理自动备份。", - "tour.api": "API 文档 — 基于 Swagger 的交互式 REST API 文档。", - "tour.search": "搜索 — 使用 Ctrl+K 快速查找并导航到任意实体。", - "tour.theme": "主题 — 在深色和浅色模式之间切换。", - "tour.accent": "主题色 — 自定义界面的强调颜色。", - "tour.language": "语言 — 选择您偏好的界面语言。", - "tour.restart": "重新开始导览", - "tour.dash.perf": "性能 — 实时 FPS 图表、延迟指标和轮询间隔控制。", - "tour.dash.running": "运行中的目标 — 实时流媒体指标和快速停止控制。", - "tour.dash.stopped": "已停止的目标 — 一键启动。", - "tour.dash.automations": "自动化 — 活动自动化状态和快速启用/禁用切换。", - "tour.tgt.led_tab": "LED 标签 — 标准 LED 灯带目标,包含设备和色带配置。", - "tour.tgt.devices": "设备 — 在网络中发现的 LED 控制器。", - "tour.tgt.css": "色带 — 定义屏幕区域如何映射到 LED 段。", - "tour.tgt.targets": "LED 目标 — 将设备、色带和捕获源组合进行流式传输。", - "tour.tgt.kc_tab": "Key Colors — 使用颜色匹配代替像素映射的替代目标类型。", - "tour.src.raw": "原始 — 来自显示器的实时屏幕捕获源。", - "tour.src.templates": "捕获模板 — 可复用的捕获配置(分辨率、FPS、裁剪)。", - "tour.src.static": "静态图片 — 使用图片文件测试您的设置。", - "tour.src.processed": "处理 — 应用后处理效果,如模糊、亮度或色彩校正。", - "tour.src.color_strip": "色带 — 定义屏幕区域如何映射到 LED 段。", - "tour.src.audio": "音频 — 分析麦克风或系统音频以实现响应式 LED 效果。", - "tour.src.value": "数值 — 用于自动化条件的数字数据源。", - "tour.src.sync": "同步时钟 — 在多个源之间同步动画的共享定时器。", - "tour.auto.list": "自动化 — 基于时间、音频或数值条件自动激活场景。", - "tour.auto.add": "点击 + 创建包含条件和要激活场景的新自动化。", - "tour.auto.card": "每张卡片显示自动化状态、条件和快速编辑/切换控制。", - "tour.auto.scenes_list": "场景 — 保存的系统状态,自动化可以激活或您可以手动应用。", - "tour.auto.scenes_add": "点击 + 将当前系统状态捕获为新的场景预设。", - "tour.auto.scenes_card": "每个场景卡片显示目标/设备数量。点击编辑、重新捕获或激活。", - "calibration.tutorial.start": "开始教程", - "calibration.overlay_toggle": "叠加层", - "calibration.start_position": "起始位置:", - "calibration.position.bottom_left": "左下", - "calibration.position.bottom_right": "右下", - "calibration.position.top_left": "左上", - "calibration.position.top_right": "右上", - "calibration.direction": "方向:", - "calibration.direction.clockwise": "顺时针", - "calibration.direction.counterclockwise": "逆时针", - "calibration.leds.top": "顶部 LED:", - "calibration.leds.right": "右侧 LED:", - "calibration.leds.bottom": "底部 LED:", - "calibration.leds.left": "左侧 LED:", - "calibration.offset": "LED 偏移:", - "calibration.offset.hint": "从物理 LED 0 到起始角落的距离(沿灯带方向)", - "calibration.skip_start": "跳过 LED(起始):", - "calibration.skip_start.hint": "灯带起始端关闭的 LED 数量(0 = 无)", - "calibration.skip_end": "跳过 LED(末尾):", - "calibration.skip_end.hint": "灯带末尾端关闭的 LED 数量(0 = 无)", - "calibration.border_width": "边框(像素):", - "calibration.border_width.hint": "从屏幕边缘采样多少像素来确定 LED 颜色(1-100)", - "calibration.button.cancel": "取消", - "calibration.button.save": "保存", - "calibration.saved": "校准已保存", - "calibration.failed": "保存校准失败", - "server.healthy": "服务器在线", - "server.offline": "服务器离线", - "error.unauthorized": "未授权 - 请先登录", - "error.network": "网络错误", - "error.unknown": "发生错误", - "modal.discard_changes": "有未保存的更改。是否放弃?", - "confirm.title": "确认操作", - "confirm.yes": "是", - "confirm.no": "否", - "confirm.stop_all": "停止所有运行中的目标?", - "confirm.turn_off_device": "关闭此设备?", - "common.loading": "加载中...", - "common.delete": "删除", - "common.remove": "移除", - "common.edit": "编辑", - "common.clone": "克隆", - "common.none": "无", - "common.none_no_cspt": "无(无处理模板)", - "common.none_no_input": "无(无输入源)", - "common.none_own_speed": "无(使用自身速度)", - "common.undo": "撤销", - "validation.required": "此字段为必填项", - "bulk.processing": "处理中…", - "api.error.timeout": "请求超时 — 请重试", - "api.error.network": "网络错误 — 请检查连接", - "palette.search": "搜索…", - "section.filter.placeholder": "筛选...", - "section.filter.reset": "清除筛选", - "tags.label": "标签", - "tags.hint": "为卡片分配标签以进行分组和筛选", - "tags.placeholder": "添加标签...", - "section.expand_all": "全部展开", - "section.collapse_all": "全部折叠", - "streams.title": "源", - "streams.description": "源定义采集管线。原始源使用采集模板从显示器采集。处理源对另一个源应用后处理。将源分配给设备。", - "streams.group.raw": "源", - "streams.group.raw_templates": "引擎模板", - "streams.group.processed": "源", - "streams.group.proc_templates": "滤镜模板", - "streams.group.css_processing": "处理模板", - "streams.group.color_strip": "色带源", - "streams.group.audio": "音频", - "streams.group.audio_templates": "音频模板", - "streams.section.streams": "源", - "streams.add": "添加源", - "streams.add.raw": "添加屏幕采集", - "streams.add.processed": "添加处理源", - "streams.edit": "编辑源", - "streams.edit.raw": "编辑屏幕采集", - "streams.edit.processed": "编辑处理源", - "streams.name": "源名称:", - "streams.name.placeholder": "我的源", - "streams.type": "类型:", - "streams.type.raw": "屏幕采集", - "streams.type.processed": "已处理", - "streams.display": "显示器:", - "streams.display.hint": "采集哪个屏幕", - "streams.capture_template": "引擎模板:", - "streams.capture_template.hint": "定义屏幕采集方式的引擎模板", - "streams.target_fps": "目标 FPS:", - "streams.target_fps.hint": "采集的目标帧率(1-90)", - "streams.source": "源:", - "streams.source.hint": "要应用处理滤镜的源", - "streams.pp_template": "滤镜模板:", - "streams.pp_template.hint": "要应用到源的滤镜模板", - "streams.description_label": "描述(可选):", - "streams.description_placeholder": "描述此源...", - "streams.created": "源创建成功", - "streams.updated": "源更新成功", - "streams.deleted": "源删除成功", - "streams.delete.confirm": "确定要删除此源吗?", - "streams.modal.loading": "加载中...", - "streams.error.load": "加载源失败", - "streams.error.required": "请填写所有必填项", - "streams.error.delete": "删除源失败", - "streams.test.title": "测试源", - "streams.test.run": "运行", - "streams.test.running": "正在测试源...", - "streams.test.duration": "采集时长(秒):", - "streams.test.error.failed": "源测试失败", - "postprocessing.title": "滤镜模板", - "postprocessing.description": "处理模板定义图像滤镜和色彩校正。将它们分配给处理图片源以实现跨设备的一致后处理。", - "postprocessing.add": "添加滤镜模板", - "postprocessing.edit": "编辑滤镜模板", - "postprocessing.name": "模板名称:", - "postprocessing.name.placeholder": "我的滤镜模板", - "filters.select_type": "选择滤镜类型...", - "filters.add": "添加滤镜", - "filters.remove": "移除", - "filters.drag_to_reorder": "拖动以重新排序", - "filters.empty": "尚未添加滤镜。使用下方选择器添加滤镜。", - "filters.brightness": "亮度", - "filters.brightness.desc": "调整整体图像亮度", - "filters.saturation": "饱和度", - "filters.saturation.desc": "增强或降低色彩强度", - "filters.gamma": "伽马", - "filters.gamma.desc": "非线性亮度曲线校正", - "filters.downscaler": "缩小", - "filters.downscaler.desc": "降低分辨率以加快处理", - "filters.pixelate": "像素化", - "filters.pixelate.desc": "马赛克式块平均", - "filters.auto_crop": "自动裁剪", - "filters.auto_crop.desc": "移除信箱式内容的黑边", - "filters.flip": "翻转", - "filters.flip.desc": "水平或垂直镜像翻转", - "filters.color_correction": "色彩校正", - "filters.color_correction.desc": "白平衡和色温调整", - "filters.filter_template": "滤镜模板", - "filters.filter_template.desc": "嵌入另一个处理模板", - "filters.css_filter_template": "色带滤镜模板", - "filters.css_filter_template.desc": "嵌入另一个色带处理模板", - "filters.frame_interpolation": "帧插值", - "filters.frame_interpolation.desc": "帧间混合以获得更平滑的输出", - "filters.noise_gate": "噪声门", - "filters.noise_gate.desc": "抑制低于阈值的细微色彩变化", - "filters.palette_quantization": "调色板量化", - "filters.palette_quantization.desc": "将颜色减少到有限调色板", - "filters.reverse": "反转", - "filters.reverse.desc": "反转色带中的LED顺序", - "postprocessing.description_label": "描述(可选):", - "postprocessing.description_placeholder": "描述此模板...", - "postprocessing.created": "模板创建成功", - "postprocessing.updated": "模板更新成功", - "postprocessing.deleted": "模板删除成功", - "postprocessing.delete.confirm": "确定要删除此滤镜模板吗?", - "postprocessing.error.load": "加载处理模板失败", - "postprocessing.error.required": "请填写所有必填项", - "postprocessing.error.delete": "删除处理模板失败", - "postprocessing.config.show": "显示设置", - "postprocessing.test.title": "测试滤镜模板", - "postprocessing.test.source_stream": "源:", - "postprocessing.test.running": "正在测试处理模板...", - "postprocessing.test.error.no_stream": "请选择一个源", - "postprocessing.test.error.failed": "处理模板测试失败", - "css_processing.title": "色带处理模板", - "css_processing.add": "添加色带处理模板", - "css_processing.edit": "编辑色带处理模板", - "css_processing.name": "模板名称:", - "css_processing.name_placeholder": "我的色带处理模板", - "css_processing.description_label": "描述(可选):", - "css_processing.description_placeholder": "描述此模板...", - "css_processing.created": "色带处理模板已创建", - "css_processing.updated": "色带处理模板已更新", - "css_processing.deleted": "色带处理模板已删除", - "css_processing.delete.confirm": "确定要删除此色带处理模板吗?", - "css_processing.error.required": "请填写所有必填字段", - "css_processing.error.load": "加载色带处理模板出错", - "css_processing.error.delete": "删除色带处理模板出错", - "css_processing.error.clone_failed": "克隆色带处理模板失败", - "device.button.stream_selector": "源设置", - "device.stream_settings.title": "源设置", - "device.stream_selector.label": "源:", - "device.stream_selector.hint": "选择一个源来定义此设备采集和处理的内容", - "device.stream_selector.none": "-- 未分配源 --", - "device.stream_selector.saved": "源设置已更新", - "device.stream_settings.border_width": "边框宽度(像素):", - "device.stream_settings.border_width_hint": "从屏幕边缘采样多少像素来确定 LED 颜色(1-100)", - "device.stream_settings.interpolation": "插值模式:", - "device.stream_settings.interpolation.average": "平均", - "device.stream_settings.interpolation.median": "中位数", - "device.stream_settings.interpolation.dominant": "主色", - "device.stream_settings.interpolation_hint": "如何从采样像素计算 LED 颜色", - "device.stream_settings.smoothing": "平滑:", - "device.stream_settings.smoothing_hint": "帧间时间混合(0=无,1=完全)。减少闪烁。", - "device.tip.stream_selector": "为此设备配置图片源和 LED 投影设置", - "streams.group.static_image": "静态图片", - "streams.add.static_image": "添加静态图片源", - "streams.edit.static_image": "编辑静态图片源", - "streams.type.static_image": "静态图片", - "streams.group.video": "视频", - "streams.add.video": "添加视频源", - "streams.edit.video": "编辑视频源", - "picture_source.type.video": "视频", - "picture_source.type.video.desc": "从上传的视频素材中流式传输帧", - "picture_source.video.loop": "循环:", - "picture_source.video.speed": "播放速度:", - "picture_source.video.start_time": "开始时间(秒):", - "picture_source.video.end_time": "结束时间(秒):", - "picture_source.video.resolution_limit": "最大宽度(像素):", - "picture_source.video.resolution_limit.hint": "解码时缩小视频以提高性能", - "streams.image_asset": "图片素材:", - "streams.image_asset.select": "选择图片素材…", - "streams.image_asset.search": "搜索图片素材…", - "streams.video_asset": "视频素材:", - "streams.video_asset.select": "选择视频素材…", - "streams.video_asset.search": "搜索视频素材…", - "targets.title": "目标", - "targets.description": "目标将色带源桥接到输出设备。每个目标引用一个设备和一个色带源。", - "targets.subtab.wled": "LED", - "targets.subtab.led": "LED", - "targets.section.devices": "设备", - "targets.section.color_strips": "色带源", - "targets.section.targets": "目标", - "targets.section.specific_settings": "特定设置", - "targets.section.advanced": "高级", - "targets.add": "添加目标", - "targets.edit": "编辑目标", - "targets.loading": "正在加载目标...", - "targets.none": "尚未配置目标", - "targets.failed": "加载目标失败", - "targets.name": "目标名称:", - "targets.name.placeholder": "我的目标", - "targets.device": "设备:", - "targets.device.hint": "选择要发送数据的 LED 设备", - "targets.device.none": "-- 选择设备 --", - "targets.color_strip_source": "色带源:", - "targets.color_strip_source.hint": "选择为此目标提供 LED 颜色的色带源", - "targets.no_css": "无源", - "targets.source": "源:", - "targets.source.hint": "要采集和处理的图片源", - "targets.source.none": "-- 未分配源 --", - "targets.metrics.pipeline": "管线详情", - "targets.fps": "目标 FPS:", - "targets.fps.hint": "采集和 LED 更新的目标帧率(1-90)", - "targets.fps.rec": "硬件最大 ≈ {fps} fps({leds} 个 LED)", - "targets.border_width": "边框宽度(像素):", - "targets.border_width.hint": "从屏幕边缘采样多少像素来确定 LED 颜色(1-100)", - "targets.interpolation": "插值模式:", - "targets.interpolation.hint": "如何从采样像素计算 LED 颜色", - "targets.interpolation.average": "平均", - "targets.interpolation.median": "中位数", - "targets.interpolation.dominant": "主色", - "targets.smoothing": "平滑:", - "targets.smoothing.hint": "帧间时间混合(0=无,1=完全)。减少闪烁。", - "targets.keepalive_interval": "心跳间隔:", - "targets.keepalive_interval.hint": "源静态时重新发送最后一帧的频率,保持设备在活动模式(0.5-5.0秒)", - "targets.created": "目标创建成功", - "targets.updated": "目标更新成功", - "targets.deleted": "目标删除成功", - "targets.delete.confirm": "确定要删除此目标吗?", - "targets.error.load": "加载目标失败", - "targets.error.required": "请填写所有必填项", - "targets.error.name_required": "请输入目标名称", - "targets.error.delete": "删除目标失败", - "targets.button.start": "启动", - "targets.button.stop": "停止", - "targets.status.processing": "处理中", - "targets.status.idle": "空闲", - "targets.status.error": "错误", - "targets.metrics.actual_fps": "实际 FPS", - "targets.metrics.target_fps": "目标 FPS", - "targets.metrics.frames": "帧数", - "targets.metrics.errors": "错误", - "targets.subtab.key_colors": "关键颜色", - "targets.section.key_colors": "关键颜色目标", - "kc.add": "添加关键颜色目标", - "kc.edit": "编辑关键颜色目标", - "kc.name": "目标名称:", - "kc.name.placeholder": "我的关键颜色目标", - "kc.source": "图片源:", - "kc.source.hint": "从哪个图片源提取颜色", - "kc.source.none": "-- 未分配源 --", - "kc.fps": "提取 FPS:", - "kc.fps.hint": "每秒提取颜色的次数(1-60)", - "kc.interpolation": "颜色模式:", - "kc.interpolation.hint": "如何从每个矩形中的像素计算关键颜色", - "kc.interpolation.average": "平均", - "kc.interpolation.median": "中位数", - "kc.interpolation.dominant": "主色", - "kc.interpolation.average.desc": "所有像素颜色的平均值", - "kc.interpolation.median.desc": "每通道中间颜色值", - "kc.interpolation.dominant.desc": "出现最频繁的颜色", - "kc.smoothing": "平滑:", - "kc.smoothing.hint": "提取间的时间混合(0=无,1=完全)", - "kc.pattern_template": "图案模板:", - "kc.pattern_template.hint": "选择用于颜色提取的矩形图案", - "kc.pattern_template.none": "-- 选择图案模板 --", - "kc.brightness_vs": "亮度源:", - "kc.brightness_vs.hint": "可选的值源,每帧动态控制亮度(与手动亮度滑块相乘)", - "kc.brightness_vs.none": "无(仅手动亮度)", - "kc.created": "关键颜色目标创建成功", - "kc.updated": "关键颜色目标更新成功", - "kc.deleted": "关键颜色目标删除成功", - "kc.delete.confirm": "确定要删除此关键颜色目标吗?", - "kc.error.no_pattern": "请选择图案模板", - "kc.error.required": "请填写所有必填项", - "kc.colors.none": "尚未提取颜色", - "kc.test": "测试", - "kc.test.error": "测试失败", - "targets.section.pattern_templates": "图案模板", - "pattern.add": "添加图案模板", - "pattern.edit": "编辑图案模板", - "pattern.name": "模板名称:", - "pattern.name.placeholder": "我的图案模板", - "pattern.description_label": "描述(可选):", - "pattern.description_placeholder": "描述此图案...", - "pattern.rectangles": "矩形", - "pattern.rect.name": "名称", - "pattern.rect.x": "X", - "pattern.rect.y": "Y", - "pattern.rect.width": "宽", - "pattern.rect.height": "高", - "pattern.rect.add": "添加矩形", - "pattern.rect.remove": "移除", - "pattern.rect.empty": "尚未定义矩形。请至少添加一个矩形。", - "pattern.created": "图案模板创建成功", - "pattern.updated": "图案模板更新成功", - "pattern.deleted": "图案模板删除成功", - "pattern.delete.confirm": "确定要删除此图案模板吗?", - "pattern.delete.referenced": "无法删除:此模板正被目标引用", - "pattern.error.required": "请填写所有必填项", - "pattern.visual_editor": "可视编辑器", - "pattern.capture_bg": "采集背景", - "pattern.source_for_bg": "背景源:", - "pattern.source_for_bg.none": "-- 选择源 --", - "pattern.delete_selected": "删除选中", - "pattern.name.hint": "此矩形布局的描述性名称", - "pattern.description.hint": "关于此图案使用位置或方式的可选说明", - "pattern.visual_editor.hint": "点击 + 按钮添加矩形。拖动边缘调整大小,拖动内部移动位置。", - "pattern.rectangles.hint": "用精确坐标(0.0 到 1.0)微调矩形位置和大小", - "overlay.toggle": "切换屏幕叠加层", - "overlay.button.show": "显示叠加层可视化", - "overlay.button.hide": "隐藏叠加层可视化", - "overlay.started": "叠加层可视化已启动", - "overlay.stopped": "叠加层可视化已停止", - "overlay.error.start": "启动叠加层失败", - "overlay.error.stop": "停止叠加层失败", - "dashboard.title": "仪表盘", - "dashboard.section.targets": "目标", - "dashboard.section.running": "运行中", - "dashboard.section.stopped": "已停止", - "dashboard.no_targets": "尚未配置目标", - "dashboard.uptime": "运行时长", - "dashboard.fps": "FPS", - "dashboard.errors": "错误", - "dashboard.device": "设备", - "dashboard.stop_all": "全部停止", - "dashboard.failed": "加载仪表盘失败", - "dashboard.section.automations": "自动化", - "dashboard.section.scenes": "场景预设", - "dashboard.section.sync_clocks": "同步时钟", - "dashboard.targets": "目标", - "dashboard.section.performance": "系统性能", - "dashboard.perf.cpu": "CPU", - "dashboard.perf.ram": "内存", - "dashboard.perf.gpu": "GPU", - "dashboard.perf.unavailable": "不可用", - "dashboard.perf.color": "图表颜色", - "dashboard.perf.mode.system": "系统", - "dashboard.perf.mode.app": "应用", - "dashboard.perf.mode.both": "全部", - "dashboard.poll_interval": "刷新间隔", - "automations.title": "自动化", - "automations.empty": "尚未配置自动化。创建一个以自动激活场景。", - "automations.add": "添加自动化", - "automations.edit": "编辑自动化", - "automations.delete.confirm": "删除自动化 \"{name}\"?", - "automations.name": "名称:", - "automations.name.hint": "此自动化的描述性名称", - "automations.name.placeholder": "我的自动化", - "automations.enabled": "启用:", - "automations.enabled.hint": "禁用的自动化即使满足条件也不会激活", - "automations.condition_logic": "条件逻辑:", - "automations.condition_logic.hint": "多个条件的组合方式:任一(或)或 全部(与)", - "automations.condition_logic.or": "任一条件(或)", - "automations.condition_logic.and": "全部条件(与)", - "automations.condition_logic.or.desc": "任一条件匹配时触发", - "automations.condition_logic.and.desc": "全部匹配时才触发", - "automations.conditions": "条件:", - "automations.conditions.hint": "决定此自动化何时激活的规则", - "automations.conditions.add": "添加条件", - "automations.conditions.empty": "无条件 — 启用后自动化始终处于活动状态", - "automations.condition.always": "始终", - "automations.condition.always.desc": "始终活跃", - "automations.condition.always.hint": "自动化启用后立即激活并保持活动。", - "automations.condition.startup": "启动", - "automations.condition.startup.desc": "服务器启动时", - "automations.condition.startup.hint": "服务器启动时激活,启用期间保持活动。", - "automations.condition.application": "应用程序", - "automations.condition.application.desc": "应用运行/聚焦", - "automations.condition.application.apps": "应用程序:", - "automations.condition.application.apps.hint": "进程名,每行一个(例如 firefox.exe)", - "automations.condition.application.browse": "浏览", - "automations.condition.application.search": "筛选进程...", - "automations.condition.application.no_processes": "未找到进程", - "automations.condition.application.match_type": "匹配类型:", - "automations.condition.application.match_type.hint": "如何检测应用程序", - "automations.condition.application.match_type.running": "运行中", - "automations.condition.application.match_type.running.desc": "进程活跃", - "automations.condition.application.match_type.topmost": "最前", - "automations.condition.application.match_type.topmost.desc": "前台窗口", - "automations.condition.application.match_type.topmost_fullscreen": "最前 + 全屏", - "automations.condition.application.match_type.topmost_fullscreen.desc": "前台 + 全屏", - "automations.condition.application.match_type.fullscreen": "全屏", - "automations.condition.application.match_type.fullscreen.desc": "任意全屏应用", - "automations.condition.time_of_day": "时段", - "automations.condition.time_of_day.desc": "时间范围", - "automations.condition.time_of_day.start_time": "开始时间:", - "automations.condition.time_of_day.end_time": "结束时间:", - "automations.condition.time_of_day.overnight_hint": "跨夜时段(如 22:00–06:00),请将开始时间设为晚于结束时间。", - "automations.condition.system_idle": "系统空闲", - "automations.condition.system_idle.desc": "空闲/活跃", - "automations.condition.system_idle.idle_minutes": "空闲超时(分钟):", - "automations.condition.system_idle.mode": "触发模式:", - "automations.condition.system_idle.when_idle": "空闲时", - "automations.condition.system_idle.when_active": "活跃时", - "automations.condition.display_state": "显示器状态", - "automations.condition.display_state.desc": "显示器开/关", - "automations.condition.display_state.state": "显示器状态:", - "automations.condition.display_state.on": "开启", - "automations.condition.display_state.off": "关闭(休眠)", - "automations.condition.mqtt": "MQTT", - "automations.condition.mqtt.desc": "MQTT 消息", - "automations.condition.mqtt.topic": "主题:", - "automations.condition.mqtt.payload": "消息内容:", - "automations.condition.mqtt.match_mode": "匹配模式:", - "automations.condition.mqtt.match_mode.exact": "精确匹配", - "automations.condition.mqtt.match_mode.contains": "包含", - "automations.condition.mqtt.match_mode.regex": "正则表达式", - "automations.condition.mqtt.hint": "当 MQTT 主题收到匹配的消息时激活", - "automations.condition.webhook": "Webhook", - "automations.condition.webhook.desc": "HTTP 回调", - "automations.condition.webhook.hint": "通过外部服务的 HTTP 请求激活(Home Assistant、IFTTT、curl 等)", - "automations.condition.webhook.url": "Webhook URL:", - "automations.condition.webhook.copy": "复制", - "automations.condition.webhook.copied": "已复制!", - "automations.condition.webhook.save_first": "请先保存自动化以生成 Webhook URL", - "automations.scene": "场景:", - "automations.scene.hint": "条件满足时激活的场景预设", - "automations.scene.search_placeholder": "搜索场景...", - "automations.scene.none_selected": "无(无场景)", - "automations.scene.none_available": "没有可用的场景", - "automations.deactivation_mode": "停用方式:", - "automations.deactivation_mode.hint": "条件不再满足时的行为", - "automations.deactivation_mode.none": "无", - "automations.deactivation_mode.none.desc": "保持当前状态", - "automations.deactivation_mode.revert": "恢复", - "automations.deactivation_mode.revert.desc": "恢复到之前的状态", - "automations.deactivation_mode.fallback_scene": "备用", - "automations.deactivation_mode.fallback_scene.desc": "激活备用场景", - "automations.deactivation_scene": "备用场景:", - "automations.deactivation_scene.hint": "自动化停用时激活的场景", - "automations.status.active": "活动", - "automations.status.inactive": "非活动", - "automations.status.disabled": "已禁用", - "automations.action.disable": "禁用", - "automations.last_activated": "上次激活", - "automations.logic.and": " 与 ", - "automations.logic.or": " 或 ", - "automations.logic.all": "全部", - "automations.logic.any": "任一", - "automations.updated": "自动化已更新", - "automations.created": "自动化已创建", - "automations.deleted": "自动化已删除", - "automations.error.name_required": "名称为必填项", - "automations.error.clone_failed": "克隆自动化失败", - "scenes.title": "场景", - "scenes.add": "捕获场景", - "scenes.edit": "编辑场景", - "scenes.name": "名称:", - "scenes.name.hint": "此场景预设的描述性名称", - "scenes.name.placeholder": "我的场景", - "scenes.description": "描述:", - "scenes.description.hint": "此场景功能的可选描述", - "scenes.targets": "目标:", - "scenes.targets.hint": "选择要包含在此场景快照中的目标", - "scenes.targets.add": "添加目标", - "scenes.targets.search_placeholder": "搜索目标...", - "scenes.capture": "捕获", - "scenes.activate": "激活场景", - "scenes.recapture": "重新捕获当前状态", - "scenes.delete": "删除场景", - "scenes.targets_count": "目标", - "scenes.captured": "场景已捕获", - "scenes.updated": "场景已更新", - "scenes.activated": "场景已激活", - "scenes.activated_partial": "场景部分激活", - "scenes.errors": "错误", - "scenes.recaptured": "场景已重新捕获", - "scenes.deleted": "场景已删除", - "scenes.recapture_confirm": "将当前状态重新捕获到\"{name}\"中?", - "scenes.delete_confirm": "删除场景\"{name}\"?", - "scenes.error.name_required": "名称为必填项", - "scenes.error.save_failed": "保存场景失败", - "scenes.error.activate_failed": "激活场景失败", - "scenes.error.recapture_failed": "重新捕获场景失败", - "scenes.error.delete_failed": "删除场景失败", - "scenes.cloned": "场景已克隆", - "scenes.error.clone_failed": "克隆场景失败", - "time.hours_minutes": "{h}时 {m}分", - "time.minutes_seconds": "{m}分 {s}秒", - "time.seconds": "{s}秒", - "dashboard.type.led": "LED", - "dashboard.type.kc": "关键颜色", - "aria.close": "关闭", - "aria.save": "保存", - "aria.cancel": "取消", - "aria.previous": "上一个", - "aria.next": "下一个", - "aria.hint": "显示提示", - "color_strip.select_type": "选择色带类型", - "color_strip.add": "添加", - "color_strip.edit": "编辑", - "color_strip.name": "名称:", - "color_strip.name.placeholder": "墙壁灯带", - "color_strip.picture_source": "图片源:", - "color_strip.picture_source.hint": "用作 LED 颜色计算输入的屏幕采集源", - "color_strip.fps": "目标 FPS:", - "color_strip.fps.hint": "LED 颜色更新的目标帧率(10-90)", - "color_strip.interpolation": "颜色模式:", - "color_strip.interpolation.hint": "如何从采样的边框像素计算 LED 颜色", - "color_strip.interpolation.average": "平均", - "color_strip.interpolation.median": "中位数", - "color_strip.interpolation.dominant": "主色", - "color_strip.interpolation.average.desc": "将所有采样像素混合为平滑颜色", - "color_strip.interpolation.median.desc": "取中间颜色值,减少异常值", - "color_strip.interpolation.dominant.desc": "使用样本中出现最频繁的颜色", - "color_strip.smoothing": "平滑:", - "color_strip.smoothing.hint": "帧间时间混合(0=无,1=完全)。减少闪烁。", - "color_strip.frame_interpolation": "帧插值:", - "color_strip.frame_interpolation.hint": "在连续采集帧之间混合,以在采集速率较低时仍以完整目标 FPS 输出。减少慢速环境过渡时的可见阶梯效应。", - "color_strip.color_corrections": "色彩校正", - "color_strip.brightness": "亮度:", - "color_strip.brightness.hint": "输出亮度倍数(0=关闭,1=不变,2=加倍)。在颜色提取后应用。", - "color_strip.saturation": "饱和度:", - "color_strip.saturation.hint": "颜色饱和度(0=灰度,1=不变,2=双倍饱和度)", - "color_strip.gamma": "伽马:", - "color_strip.gamma.hint": "伽马校正(1=无,<1=更亮的中间调,>1=更暗的中间调)", - "color_strip.test_device": "测试设备:", - "color_strip.test_device.hint": "选择一个设备,在点击边缘切换时发送测试像素", - "color_strip.leds": "LED 数量", - "color_strip.led_count": "LED 数量:", - "color_strip.led_count.hint": "物理灯带上的 LED 总数。屏幕源:0 = 从校准自动获取(未映射到边缘的额外 LED 将为黑色)。静态颜色:设置为与设备 LED 数量匹配。", - "color_strip.created": "色带源已创建", - "color_strip.updated": "色带源已更新", - "color_strip.deleted": "色带源已删除", - "color_strip.delete.confirm": "确定要删除此色带源吗?", - "color_strip.delete.referenced": "无法删除:此源正在被目标使用", - "color_strip.error.name_required": "请输入名称", - "color_strip.type": "类型:", - "color_strip.type.hint": "图片源从屏幕采集推导 LED 颜色。静态颜色用单一颜色填充所有 LED。渐变在所有 LED 上分布颜色渐变。颜色循环平滑循环用户定义的颜色列表。组合将多个源作为混合图层叠加。音频响应从实时音频输入驱动 LED。API 输入通过 REST 或 WebSocket 从外部客户端接收原始 LED 颜色。", - "color_strip.type.picture": "图片源", - "color_strip.type.picture.desc": "从屏幕捕获获取颜色", - "color_strip.type.picture_advanced": "多显示器", - "color_strip.type.picture_advanced.desc": "跨显示器的线条校准", - "color_strip.type.static": "静态颜色", - "color_strip.type.static.desc": "单色填充", - "color_strip.type.gradient": "渐变", - "color_strip.type.gradient.desc": "LED上的平滑颜色过渡", - "color_strip.type.color_cycle": "颜色循环", - "color_strip.type.color_cycle.desc": "循环切换颜色列表", - "color_strip.static_color": "颜色:", - "color_strip.static_color.hint": "将发送到灯带上所有 LED 的纯色。", - "color_strip.gradient.preview": "渐变:", - "color_strip.gradient.preview.hint": "可视预览。点击下方标记轨道添加色标。拖动标记重新定位。", - "color_strip.gradient.stops": "色标:", - "color_strip.gradient.stops.hint": "每个色标在相对位置定义一种颜色(0.0 = 起始,1.0 = 结束)。↔ 按钮添加右侧颜色以在该色标处创建硬边。", - "color_strip.gradient.stops_count": "个色标", - "color_strip.gradient.add_stop": "+ 添加色标", - "color_strip.gradient.position": "位置(0.0-1.0)", - "color_strip.gradient.bidir.hint": "在此色标右侧添加第二种颜色以在渐变中创建硬边。", - "color_strip.gradient.min_stops": "渐变至少需要 2 个色标", - "color_strip.gradient.preset": "预设:", - "color_strip.gradient.preset.hint": "加载预定义的渐变调色板。选择预设将替换当前色标。", - "color_strip.gradient.preset.custom": "— 自定义 —", - "color_strip.gradient.preset.rainbow": "彩虹", - "color_strip.gradient.preset.sunset": "日落", - "color_strip.gradient.preset.ocean": "海洋", - "color_strip.gradient.preset.forest": "森林", - "color_strip.gradient.preset.fire": "火焰", - "color_strip.gradient.preset.lava": "熔岩", - "color_strip.gradient.preset.aurora": "极光", - "color_strip.gradient.preset.ice": "冰", - "color_strip.gradient.preset.warm": "暖色", - "color_strip.gradient.preset.cool": "冷色", - "color_strip.gradient.preset.neon": "霓虹", - "color_strip.gradient.preset.pastel": "柔和", - "color_strip.gradient.preset.save_button": "保存为预设…", - "color_strip.gradient.preset.save_prompt": "输入预设名称:", - "color_strip.gradient.preset.saved": "预设已保存", - "color_strip.gradient.preset.deleted": "预设已删除", - "color_strip.gradient.preset.apply": "应用", - "color_strip.animation": "动画", - "color_strip.animation.type": "效果:", - "color_strip.animation.type.hint": "要应用的动画效果。", - "color_strip.animation.type.none": "无(无动画效果)", - "color_strip.animation.type.none.desc": "静态颜色,无动画", - "color_strip.animation.type.breathing": "呼吸", - "color_strip.animation.type.breathing.desc": "平滑的亮度渐入渐出", - "color_strip.animation.type.color_cycle": "颜色循环", - "color_strip.animation.type.gradient_shift": "渐变移动", - "color_strip.animation.type.gradient_shift.desc": "渐变沿灯带滑动", - "color_strip.animation.type.wave": "波浪", - "color_strip.animation.type.wave.desc": "沿灯带移动的正弦亮度波", - "color_strip.animation.type.strobe": "频闪", - "color_strip.animation.type.strobe.desc": "快速开/关闪烁", - "color_strip.animation.type.sparkle": "闪烁", - "color_strip.animation.type.sparkle.desc": "随机 LED 短暂闪亮", - "color_strip.animation.type.pulse": "脉冲", - "color_strip.animation.type.pulse.desc": "快速衰减的尖锐亮度脉冲", - "color_strip.animation.type.candle": "烛光", - "color_strip.animation.type.candle.desc": "温暖的类似蜡烛的闪烁光芒", - "color_strip.animation.type.rainbow_fade": "彩虹渐变", - "color_strip.animation.type.rainbow_fade.desc": "循环整个色相光谱", - "color_strip.animation.speed": "速度:", - "color_strip.animation.speed.hint": "动画速度倍数。1.0 ≈ 呼吸效果每秒一个循环;更高值循环更快。", - "color_strip.color_cycle.colors": "颜色:", - "color_strip.color_cycle.colors.hint": "平滑循环的颜色列表。至少需要 2 种。默认为全彩虹光谱。", - "color_strip.color_cycle.add_color": "+ 添加颜色", - "color_strip.color_cycle.speed": "速度:", - "color_strip.color_cycle.speed.hint": "循环速度倍数。1.0 ≈ 每 20 秒一个完整循环;更高值循环更快。", - "color_strip.color_cycle.min_colors": "颜色循环至少需要 2 种颜色", - "color_strip.type.effect": "效果", - "color_strip.type.effect.desc": "程序化效果:火焰、等离子、极光", - "color_strip.type.effect.hint": "实时生成的程序化 LED 效果(火焰、流星、等离子、噪声、极光)。", - "color_strip.type.composite": "组合", - "color_strip.type.composite.desc": "叠加和混合多个源", - "color_strip.type.composite.hint": "将多个色带源作为图层叠加,支持混合模式和不透明度。", - "color_strip.type.mapped": "映射", - "color_strip.type.mapped.desc": "为LED区域分配源", - "color_strip.type.mapped.hint": "将不同色带源分配到不同 LED 范围(区域)。与组合的图层混合不同,映射将源并排放置。", - "color_strip.type.audio": "音频响应", - "color_strip.type.audio.desc": "由音频输入驱动LED", - "color_strip.type.audio.hint": "LED 颜色由实时音频输入驱动 — 系统音频或麦克风。", - "color_strip.type.api_input": "API 输入", - "color_strip.type.api_input.desc": "从外部应用接收颜色", - "color_strip.type.api_input.hint": "通过 REST POST 或 WebSocket 从外部客户端接收原始 LED 颜色数组。用于与自定义软件、家庭自动化或任何能发送 HTTP 请求的系统集成。", - "color_strip.api_input.fallback_color": "备用颜色:", - "color_strip.api_input.fallback_color.hint": "超时未收到数据时显示的颜色。启动时和连接丢失后 LED 将显示此颜色。", - "color_strip.api_input.timeout": "超时(秒):", - "color_strip.api_input.timeout.hint": "等待新颜色数据多长时间后恢复为备用颜色。设为 0 表示永不超时。", - "color_strip.api_input.endpoints": "推送端点:", - "color_strip.api_input.endpoints.hint": "使用这些 URL 从外部应用程序推送 LED 颜色数据。REST 接受 JSON,WebSocket 接受 JSON 和原始二进制帧。", - "color_strip.api_input.save_first": "请先保存源以查看推送端点 URL。", - "color_strip.api_input.interpolation": "LED 插值:", - "color_strip.api_input.interpolation.hint": "当传入的 LED 数量与设备 LED 数量不同时如何调整大小。线性提供平滑混合,最近邻保持锐利边缘,无则截断或补零。", - "color_strip.api_input.interpolation.linear": "线性", - "color_strip.api_input.interpolation.linear.desc": "LED 之间平滑混合", - "color_strip.api_input.interpolation.nearest": "最近邻", - "color_strip.api_input.interpolation.nearest.desc": "锐利边缘,无混合", - "color_strip.api_input.interpolation.none": "无", - "color_strip.api_input.interpolation.none.desc": "截断或补零", - "color_strip.type.notification": "通知", - "color_strip.type.notification.desc": "通过Webhook触发的一次性效果", - "color_strip.type.notification.hint": "通过 Webhook 触发时显示一次性视觉效果(闪烁、脉冲、扫描)。设计为组合源中的叠加层。", - "color_strip.notification.os_listener": "监听系统通知:", - "color_strip.notification.os_listener.hint": "启用后,当桌面通知出现时(Windows toast / Linux D-Bus),此源会自动触发。需要应用具有通知访问权限。", - "color_strip.notification.effect": "效果:", - "color_strip.notification.effect.hint": "通知触发时的视觉效果。闪烁线性衰减,脉冲平滑钟形曲线,扫描从左到右填充后衰减。", - "color_strip.notification.effect.flash": "闪烁", - "color_strip.notification.effect.flash.desc": "瞬时点亮,线性衰减", - "color_strip.notification.effect.pulse": "脉冲", - "color_strip.notification.effect.pulse.desc": "平滑钟形发光", - "color_strip.notification.effect.sweep": "扫描", - "color_strip.notification.effect.sweep.desc": "从左到右填充然后消失", - "color_strip.notification.duration": "持续时间(毫秒):", - "color_strip.notification.duration.hint": "通知效果播放的时长(毫秒)。", - "color_strip.notification.default_color": "默认颜色:", - "color_strip.notification.default_color.hint": "当通知没有应用特定颜色映射时使用的颜色。", - "color_strip.notification.filter_mode": "应用过滤:", - "color_strip.notification.filter_mode.hint": "按应用名称过滤通知。关闭=接受全部,白名单=仅列出的应用,黑名单=排除列出的应用。", - "color_strip.notification.filter_mode.off": "关闭", - "color_strip.notification.filter_mode.whitelist": "白名单", - "color_strip.notification.filter_mode.blacklist": "黑名单", - "color_strip.notification.filter_mode.off.desc": "接受所有通知", - "color_strip.notification.filter_mode.whitelist.desc": "仅列出的应用", - "color_strip.notification.filter_mode.blacklist.desc": "排除列出的应用", - "color_strip.notification.filter_list": "应用列表:", - "color_strip.notification.filter_list.hint": "每行一个应用名称。使用「浏览」从运行中的进程中选择。", - "color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram", - "color_strip.notification.app_colors": "应用颜色", - "color_strip.notification.app_colors.label": "颜色映射:", - "color_strip.notification.app_colors.hint": "每个应用的自定义通知颜色。每行将一个应用名称映射到特定颜色。", - "color_strip.notification.app_colors.add": "+ 添加映射", - "color_strip.notification.app_overrides": "按应用覆盖", - "color_strip.notification.app_overrides.label": "应用覆盖:", - "color_strip.notification.app_overrides.hint": "为特定应用自定义颜色和声音。每行可设置颜色、声音资源和音量。", - "color_strip.notification.app_overrides.add": "+ 添加覆盖", - "color_strip.notification.app_overrides.app_placeholder": "应用名称", - "color_strip.notification.sound": "声音", - "color_strip.notification.sound.asset": "声音资源:", - "color_strip.notification.sound.asset.hint": "选择通知触发时播放的声音资源。留空表示静音。", - "color_strip.notification.sound.none": "无(静音)", - "color_strip.notification.sound.search": "搜索声音…", - "color_strip.notification.sound.volume": "音量:", - "color_strip.notification.sound.volume.hint": "通知声音的全局音量(0–100%)。", - "color_strip.notification.sound.app_sounds": "按应用声音:", - "color_strip.notification.sound.app_sounds.hint": "为特定应用覆盖声音和音量。空声音 = 静音该应用。", - "color_strip.notification.sound.app_sounds.add": "+ 添加覆盖", - "color_strip.notification.sound.app_name_placeholder": "应用名称", - "color_strip.notification.endpoint": "Webhook 端点:", - "color_strip.notification.endpoint.hint": "使用此 URL 从外部系统触发通知。POST 请求可选 JSON:{\"app\": \"AppName\", \"color\": \"#FF0000\"}。", - "color_strip.notification.save_first": "请先保存源以查看 Webhook 端点 URL。", - "color_strip.notification.app_count": "个应用", - "color_strip.notification.test": "测试通知", - "color_strip.notification.test.ok": "通知已发送", - "color_strip.notification.test.no_streams": "此源没有运行中的流", - "color_strip.notification.test.error": "发送通知失败", - "color_strip.notification.history.title": "通知历史", - "color_strip.notification.history.hint": "监听器捕获的最近OS通知(最新在前),最多50条。", - "color_strip.notification.history.empty": "尚未捕获任何通知", - "color_strip.notification.history.unavailable": "此平台不支持OS通知监听器", - "color_strip.notification.history.error": "加载通知历史失败", - "color_strip.notification.history.refresh": "刷新", - "color_strip.notification.history.unknown_app": "未知应用", - "color_strip.notification.history.fired": "触发的流数量", - "color_strip.notification.history.filtered": "过滤的流数量", - "color_strip.test.title": "预览测试", - "color_strip.test.connecting": "连接中...", - "color_strip.test.error": "无法连接到预览流", - "color_strip.test.led_count": "LED数量:", - "color_strip.test.fps": "FPS:", - "color_strip.test.receive_fps": "接收帧率", - "color_strip.test.apply": "应用", - "color_strip.test.composite": "合成", - "color_strip.preview.title": "实时预览", - "color_strip.preview.not_connected": "未连接", - "color_strip.preview.connecting": "连接中...", - "color_strip.preview.connected": "已连接", - "color_strip.preview.unsupported": "此源类型不支持预览", - "color_strip.type.daylight": "日光循环", - "color_strip.type.daylight.desc": "模拟24小时自然日光变化", - "color_strip.type.daylight.hint": "模拟太阳在24小时内的色温变化——从温暖的日出到冷白的日光,再到温暖的日落和昏暗的夜晚。", - "color_strip.daylight.speed": "速度:", - "color_strip.daylight.speed.hint": "循环速度倍数。1.0 = 约4分钟完成一个完整的昼夜循环。", - "color_strip.daylight.use_real_time": "使用实时时间:", - "color_strip.daylight.use_real_time.hint": "启用后,LED颜色匹配计算机的实际时间。速度设置将被忽略。", - "color_strip.daylight.real_time": "实时", - "color_strip.daylight.latitude": "纬度:", - "color_strip.daylight.latitude.hint": "地理纬度(-90到90)。影响实时模式下的日出/日落时间。", - "color_strip.type.candlelight": "烛光", - "color_strip.type.candlelight.desc": "逼真的烛光闪烁模拟", - "color_strip.type.candlelight.hint": "在所有LED上模拟逼真的蜡烛闪烁,具有温暖色调和有机闪烁模式。", - "color_strip.type.weather": "天气", - "color_strip.type.weather.desc": "天气感应环境色彩", - "color_strip.type.weather.hint": "将实时天气状况映射为环境LED颜色。需要天气源实体。", - "color_strip.weather.source": "天气源:", - "color_strip.weather.source.hint": "用于环境颜色的天气数据源。请先在天气标签页中创建。", - "color_strip.weather.speed": "动画速度:", - "color_strip.weather.speed.hint": "环境色彩漂移动画速度。越高越快。", - "color_strip.weather.temperature_influence": "温度影响:", - "color_strip.weather.temperature_influence.hint": "当前温度对调色板冷暖偏移程度。0=纯天气颜色,1=强偏移。", - "color_strip.weather.error.no_source": "请选择天气源", - "color_strip.candlelight.color": "基础颜色:", - "color_strip.candlelight.color.hint": "蜡烛火焰的温暖基础颜色。默认为自然温暖的琥珀色。", - "color_strip.candlelight.intensity": "闪烁强度:", - "color_strip.candlelight.intensity.hint": "蜡烛闪烁程度。低值产生柔和光芒,高值模拟风中的蜡烛。", - "color_strip.candlelight.num_candles_label": "蜡烛数量:", - "color_strip.candlelight.num_candles": "支蜡烛", - "color_strip.candlelight.num_candles.hint": "灯带上独立蜡烛光源的数量。每支蜡烛有自己的闪烁模式。", - "color_strip.candlelight.speed": "闪烁速度:", - "color_strip.candlelight.speed.hint": "闪烁动画的速度。较高的值产生更快、更不安定的火焰。", - "color_strip.type.processed": "已处理", - "color_strip.type.processed.desc": "将处理模板应用于另一个源", - "color_strip.type.processed.hint": "包装现有色带源并通过滤镜链处理其输出。", - "color_strip.processed.input": "源:", - "color_strip.processed.input.hint": "将被处理的色带源", - "color_strip.processed.template": "处理模板:", - "color_strip.processed.template.hint": "应用于输入源输出的滤镜链", - "color_strip.processed.error.no_input": "请选择输入源", - "color_strip.composite.layers": "图层:", - "color_strip.composite.layers.hint": "叠加多个色带源。第一个图层在底部,最后一个在顶部。每个图层可以有自己的混合模式和不透明度。", - "color_strip.composite.add_layer": "+ 添加图层", - "color_strip.composite.source": "源", - "color_strip.composite.blend_mode": "混合", - "color_strip.composite.blend_mode.normal": "正常", - "color_strip.composite.blend_mode.normal.desc": "标准 Alpha 混合", - "color_strip.composite.blend_mode.add": "叠加", - "color_strip.composite.blend_mode.add.desc": "通过叠加颜色提亮", - "color_strip.composite.blend_mode.multiply": "正片叠底", - "color_strip.composite.blend_mode.multiply.desc": "通过相乘颜色变暗", - "color_strip.composite.blend_mode.screen": "滤色", - "color_strip.composite.blend_mode.screen.desc": "提亮,正片叠底的反转", - "color_strip.composite.blend_mode.override": "覆盖", - "color_strip.composite.blend_mode.overlay": "叠加", - "color_strip.composite.blend_mode.overlay.desc": "暗部相乘,亮部滤色", - "color_strip.composite.blend_mode.soft_light": "柔光", - "color_strip.composite.blend_mode.soft_light.desc": "柔和对比度调整", - "color_strip.composite.blend_mode.hard_light": "强光", - "color_strip.composite.blend_mode.hard_light.desc": "强对比度,鲜艳色彩", - "color_strip.composite.blend_mode.difference": "差值", - "color_strip.composite.blend_mode.difference.desc": "绝对颜色差异", - "color_strip.composite.blend_mode.exclusion": "排除", - "color_strip.composite.blend_mode.exclusion.desc": "类似差值,对比度更低", - "color_strip.composite.blend_mode.override.desc": "黑色=透明,亮色=不透明", - "color_strip.composite.opacity": "不透明度", - "color_strip.composite.brightness": "亮度", - "color_strip.composite.brightness.none": "无(全亮度)", - "color_strip.composite.processing": "处理", - "color_strip.composite.enabled": "启用", - "color_strip.composite.error.min_layers": "至少需要 1 个图层", - "color_strip.composite.error.no_source": "每个图层必须选择一个源", - "color_strip.composite.layers_count": "个图层", - "color_strip.composite.range": "LED范围", - "color_strip.composite.range_start": "起始", - "color_strip.composite.range_end": "结束", - "color_strip.composite.reverse": "反转", - "color_strip.mapped.zones": "区域:", - "color_strip.mapped.zones.hint": "每个区域将色带源映射到特定 LED 范围。区域并排放置 — 区域之间的间隙保持黑色。", - "color_strip.mapped.add_zone": "+ 添加区域", - "color_strip.mapped.zone_source": "源", - "color_strip.mapped.zone_start": "起始 LED", - "color_strip.mapped.zone_end": "结束 LED", - "color_strip.mapped.zone_reverse": "反转", - "color_strip.mapped.zones_count": "个区域", - "color_strip.mapped.select_source": "搜索源...", - "color_strip.mapped.error.no_source": "每个区域必须选择一个源", - "color_strip.audio.visualization": "可视化:", - "color_strip.audio.visualization.hint": "音频数据如何渲染到 LED。", - "color_strip.audio.viz.spectrum": "频谱分析", - "color_strip.audio.viz.spectrum.desc": "频率条分布在灯带上", - "color_strip.audio.viz.beat_pulse": "节拍脉冲", - "color_strip.audio.viz.beat_pulse.desc": "所有LED随节拍脉动", - "color_strip.audio.viz.vu_meter": "VU 表", - "color_strip.audio.viz.vu_meter.desc": "音量填充灯带", - "color_strip.audio.source": "音频源:", - "color_strip.audio.source.hint": "此可视化的音频源。可以是多声道(设备)或单声道(单通道)源。在源标签页中创建和管理音频源。", - "color_strip.audio.sensitivity": "灵敏度:", - "color_strip.audio.sensitivity.hint": "音频电平的增益倍数。更高值使 LED 对较安静的声音也有反应。", - "color_strip.audio.smoothing": "平滑:", - "color_strip.audio.smoothing.hint": "帧间时间平滑。更高值产生更平滑但反应较慢的视觉效果。", - "color_strip.audio.palette": "调色板:", - "color_strip.audio.palette.hint": "用于频谱条或节拍脉冲着色的调色板。", - "color_strip.audio.color": "基础颜色:", - "color_strip.audio.color.hint": "VU 表条的低电平颜色。", - "color_strip.audio.color_peak": "峰值颜色:", - "color_strip.audio.color_peak.hint": "VU 表条顶部的高电平颜色。", - "color_strip.audio.mirror": "镜像:", - "color_strip.audio.mirror.hint": "从中心向外镜像频谱:低音在中间,高音在两端。", - "color_strip.effect.type": "效果类型:", - "color_strip.effect.type.hint": "选择程序化算法。", - "color_strip.effect.fire": "火焰", - "color_strip.effect.fire.desc": "模拟带热量扩散的上升火焰的元胞自动机", - "color_strip.effect.meteor": "流星", - "color_strip.effect.meteor.desc": "明亮头部沿灯带移动,带指数衰减的尾迹", - "color_strip.effect.plasma": "等离子", - "color_strip.effect.plasma.desc": "映射到调色板的重叠正弦波 — 经典演示场景效果", - "color_strip.effect.noise": "噪声", - "color_strip.effect.noise.desc": "滚动的分形值噪声映射到调色板", - "color_strip.effect.aurora": "极光", - "color_strip.effect.aurora.desc": "漂移和混合的分层噪声带 — 北极光风格", - "color_strip.effect.speed": "速度:", - "color_strip.effect.speed.hint": "效果动画的速度倍数(0.1 = 非常慢,10.0 = 非常快)。", - "color_strip.effect.palette": "调色板:", - "color_strip.effect.palette.hint": "用于将效果值映射到 RGB 颜色的调色板。", - "color_strip.effect.color": "流星颜色:", - "color_strip.effect.color.hint": "流星效果的头部颜色。", - "color_strip.effect.intensity": "强度:", - "color_strip.effect.intensity.hint": "效果强度 — 控制火花率(火焰)、尾迹衰减(流星)或亮度范围(极光)。", - "color_strip.effect.scale": "比例:", - "color_strip.effect.scale.hint": "空间比例 — 波频率(等离子)、缩放级别(噪声)或带宽(极光)。", - "color_strip.effect.mirror": "镜像:", - "color_strip.effect.mirror.hint": "反弹模式 — 流星在灯带末端反转方向而不是循环。", - "color_strip.palette.fire": "火焰", - "color_strip.palette.ocean": "海洋", - "color_strip.palette.lava": "熔岩", - "color_strip.palette.forest": "森林", - "color_strip.palette.rainbow": "彩虹", - "color_strip.palette.aurora": "极光", - "color_strip.palette.sunset": "日落", - "color_strip.palette.ice": "冰", - "audio_source.title": "音频源", - "audio_source.group.multichannel": "多声道", - "audio_source.group.mono": "单声道", - "audio_source.group.band_extract": "频段提取", - "audio_source.add": "添加音频源", - "audio_source.add.multichannel": "添加多声道源", - "audio_source.add.mono": "添加单声道源", - "audio_source.add.band_extract": "添加频段提取源", - "audio_source.edit": "编辑音频源", - "audio_source.edit.multichannel": "编辑多声道源", - "audio_source.edit.mono": "编辑单声道源", - "audio_source.edit.band_extract": "编辑频段提取源", - "audio_source.name": "名称:", - "audio_source.name.placeholder": "系统音频", - "audio_source.name.hint": "此音频源的描述性名称", - "audio_source.type": "类型:", - "audio_source.type.hint": "多声道从物理音频设备采集所有通道。单声道从多声道源提取单个通道。", - "audio_source.type.multichannel": "多声道", - "audio_source.type.mono": "单声道", - "audio_source.device": "音频设备:", - "audio_source.device.hint": "音频输入源。回环设备采集系统音频输出;输入设备采集麦克风或线路输入。", - "audio_source.refresh_devices": "刷新设备", - "audio_source.parent": "父源:", - "audio_source.parent.hint": "要从中提取通道的多声道源", - "audio_source.channel": "通道:", - "audio_source.channel.hint": "从多声道源提取哪个音频通道", - "audio_source.channel.mono": "单声道(左+右混合)", - "audio_source.channel.left": "左", - "audio_source.channel.right": "右", - "audio_source.description": "描述(可选):", - "audio_source.description.placeholder": "描述此音频源...", - "audio_source.description.hint": "关于此音频源的可选说明", - "audio_source.created": "音频源已创建", - "audio_source.updated": "音频源已更新", - "audio_source.deleted": "音频源已删除", - "audio_source.delete.confirm": "确定要删除此音频源吗?", - "audio_source.error.name_required": "请输入名称", - "audio_source.audio_template": "音频模板:", - "audio_source.audio_template.hint": "定义此设备使用哪个引擎和设置的音频采集模板", - "audio_source.band_parent": "父音频源:", - "audio_source.band_parent.hint": "要从中提取频段的音频源", - "audio_source.band": "频段:", - "audio_source.band.hint": "选择频段预设或自定义范围", - "audio_source.band.bass": "低音 (20–250 Hz)", - "audio_source.band.mid": "中音 (250–4000 Hz)", - "audio_source.band.treble": "高音 (4000–20000 Hz)", - "audio_source.band.custom": "自定义范围", - "audio_source.freq_low": "低频 (Hz):", - "audio_source.freq_high": "高频 (Hz):", - "audio_source.freq_range": "频率范围", - "audio_source.test": "测试", - "audio_source.test.title": "测试音频源", - "audio_source.test.rms": "RMS", - "audio_source.test.peak": "峰值", - "audio_source.test.beat": "节拍", - "audio_source.test.connecting": "连接中...", - "audio_source.test.error": "音频测试失败", - "audio_template.test": "测试", - "audio_template.test.title": "测试音频模板", - "audio_template.test.device": "音频设备:", - "audio_template.test.device.hint": "选择测试期间要采集的音频设备", - "audio_template.test.run": "运行", - "audio_template.title": "音频模板", - "audio_template.add": "添加音频模板", - "audio_template.edit": "编辑音频模板", - "audio_template.name": "模板名称:", - "audio_template.name.placeholder": "我的音频模板", - "audio_template.description.label": "描述(可选):", - "audio_template.description.placeholder": "描述此模板...", - "audio_template.engine": "音频引擎:", - "audio_template.engine.hint": "选择要使用的音频采集后端。WASAPI 仅限 Windows 并支持回环。Sounddevice 支持跨平台。", - "audio_template.engine.unavailable": "不可用", - "audio_template.engine.unavailable.hint": "此引擎在您的系统上不可用", - "audio_template.config": "配置", - "audio_template.config.show": "显示配置", - "audio_template.created": "音频模板已创建", - "audio_template.updated": "音频模板已更新", - "audio_template.deleted": "音频模板已删除", - "audio_template.delete.confirm": "确定要删除此音频模板吗?", - "audio_template.error.load": "加载音频模板失败", - "audio_template.error.engines": "加载音频引擎失败", - "audio_template.error.required": "请填写所有必填项", - "audio_template.error.delete": "删除音频模板失败", - "streams.group.value": "值源", - "streams.group.sync": "同步时钟", - "tree.group.picture": "图片源", - "tree.group.capture": "屏幕采集", - "tree.group.static": "静态", - "tree.group.processing": "已处理", - "tree.group.strip": "色带", - "tree.group.audio": "音频", - "tree.group.utility": "工具", - "tree.leaf.sources": "源", - "tree.leaf.engine_templates": "引擎模板", - "tree.leaf.images": "图片", - "tree.leaf.video": "视频", - "tree.leaf.filter_templates": "滤镜模板", - "tree.leaf.processing_templates": "处理模板", - "tree.leaf.templates": "模板", - "value_source.group.title": "值源", - "value_source.select_type": "选择值源类型", - "value_source.add": "添加值源", - "value_source.edit": "编辑值源", - "value_source.name": "名称:", - "value_source.name.placeholder": "亮度脉冲", - "value_source.name.hint": "此值源的描述性名称", - "value_source.type": "类型:", - "value_source.type.hint": "静态输出固定值。动画循环波形。音频响应声音输入。自适应类型根据时间或场景内容自动调节亮度。", - "value_source.type.static": "静态", - "value_source.type.static.desc": "固定输出值", - "value_source.type.animated": "动画", - "value_source.type.animated.desc": "循环波形变化", - "value_source.type.audio": "音频", - "value_source.type.audio.desc": "响应声音输入", - "value_source.type.adaptive_time": "自适应(时间)", - "value_source.type.adaptive_time.desc": "按时间自动调节", - "value_source.type.adaptive_scene": "自适应(场景)", - "value_source.type.adaptive_scene.desc": "按场景内容调节", - "value_source.type.daylight": "日光周期", - "value_source.type.daylight.desc": "亮度跟随日夜周期", - "value_source.daylight.speed": "速度:", - "value_source.daylight.speed.hint": "周期速度倍率。1.0 = 完整日夜周期约4分钟。", - "value_source.daylight.use_real_time": "使用实时:", - "value_source.daylight.use_real_time.hint": "启用后,亮度跟随实际时间。速度设置将被忽略。", - "value_source.daylight.enable_real_time": "跟随系统时钟", - "value_source.daylight.latitude": "纬度:", - "value_source.daylight.latitude.hint": "地理纬度(-90到90)。影响实时模式下的日出/日落时间。", - "value_source.daylight.real_time": "实时", - "value_source.daylight.speed_label": "速度", - "value_source.value": "值:", - "value_source.value.hint": "固定输出值(0.0 = 关闭,1.0 = 最大亮度)", - "value_source.waveform": "波形:", - "value_source.waveform.hint": "亮度动画循环的形状", - "value_source.waveform.sine": "正弦", - "value_source.waveform.triangle": "三角", - "value_source.waveform.square": "方波", - "value_source.waveform.sawtooth": "锯齿", - "value_source.speed": "速度(周期/分):", - "value_source.speed.hint": "每分钟周期数 — 波形重复的速度(1 = 非常慢,120 = 非常快)", - "value_source.min_value": "最小值:", - "value_source.min_value.hint": "波形周期的最小输出", - "value_source.max_value": "最大值:", - "value_source.max_value.hint": "波形周期的最大输出", - "value_source.audio_source": "音频源:", - "value_source.audio_source.hint": "要读取音频电平的音频源(多声道或单声道)", - "value_source.mode": "模式:", - "value_source.mode.hint": "RMS 测量平均音量。峰值跟踪最响的时刻。节拍在节奏上触发。", - "value_source.mode.rms": "RMS(音量)", - "value_source.mode.peak": "峰值", - "value_source.mode.beat": "节拍", - "value_source.mode.rms.desc": "平均音量水平", - "value_source.mode.peak.desc": "最响时刻追踪", - "value_source.mode.beat.desc": "节奏脉冲检测", - "value_source.auto_gain": "自动增益:", - "value_source.auto_gain.hint": "自动归一化音频电平,使输出使用完整范围,无论输入音量大小", - "value_source.auto_gain.enable": "启用自动增益", - "value_source.sensitivity": "灵敏度:", - "value_source.sensitivity.hint": "音频信号的增益倍数(越高反应越灵敏)", - "value_source.scene_sensitivity.hint": "亮度信号的增益倍数(越高对亮度变化越敏感)", - "value_source.smoothing": "平滑:", - "value_source.smoothing.hint": "时间平滑(0 = 即时响应,1 = 非常平滑/缓慢)", - "value_source.audio_min_value": "最小值:", - "value_source.audio_min_value.hint": "音频静默时的输出(例如 0.3 = 30% 亮度下限)", - "value_source.audio_max_value": "最大值:", - "value_source.audio_max_value.hint": "最大音频电平时的输出", - "value_source.schedule": "计划:", - "value_source.schedule.hint": "定义至少 2 个时间点。亮度在各点之间线性插值,午夜循环。", - "value_source.schedule.add": "+ 添加时间点", - "value_source.schedule.points": "个时间点", - "value_source.picture_source": "图片源:", - "value_source.picture_source.hint": "将分析其帧以获取平均亮度的图片源。", - "value_source.scene_behavior": "行为:", - "value_source.scene_behavior.hint": "互补:暗场景 = 高亮度(适合环境背光)。匹配:亮场景 = 高亮度。", - "value_source.scene_behavior.complement": "互补(暗 → 亮)", - "value_source.scene_behavior.match": "匹配(亮 → 亮)", - "value_source.adaptive_min_value": "最小值:", - "value_source.adaptive_min_value.hint": "最小输出亮度", - "value_source.adaptive_max_value": "最大值:", - "value_source.adaptive_max_value.hint": "最大输出亮度", - "value_source.error.schedule_min": "计划至少需要 2 个时间点", - "value_source.description": "描述(可选):", - "value_source.description.placeholder": "描述此值源...", - "value_source.description.hint": "关于此值源的可选说明", - "value_source.created": "值源已创建", - "value_source.updated": "值源已更新", - "value_source.deleted": "值源已删除", - "value_source.delete.confirm": "确定要删除此值源吗?", - "value_source.error.name_required": "请输入名称", - "value_source.test": "测试", - "value_source.test.title": "测试值源", - "value_source.test.connecting": "连接中...", - "value_source.test.error": "连接失败", - "value_source.test.current": "当前", - "value_source.test.min": "最小", - "value_source.test.max": "最大", - "test.frames": "帧数", - "test.fps": "帧率", - "test.avg_capture": "平均", - "targets.brightness_vs": "亮度源:", - "targets.brightness_vs.hint": "可选的值源,每帧动态控制亮度(覆盖设备亮度)", - "targets.brightness_vs.none": "无(设备亮度)", - "targets.min_brightness_threshold": "最低亮度阈值:", - "targets.min_brightness_threshold.hint": "当有效输出亮度(像素亮度 × 设备/源亮度)低于此值时,LED完全关闭(0 = 禁用)", - "targets.adaptive_fps": "自适应FPS:", - "targets.adaptive_fps.hint": "当设备无响应时自动降低发送速率,稳定后逐步恢复。推荐用于信号较弱的WiFi设备。", - "targets.protocol": "协议:", - "targets.protocol.hint": "DDP通过快速UDP发送像素(推荐)。HTTP使用JSON API——较慢但可靠,限制约500个LED。", - "targets.protocol.ddp": "DDP (UDP)", - "targets.protocol.ddp.desc": "快速UDP数据包 - 推荐", - "targets.protocol.http": "HTTP", - "targets.protocol.http.desc": "JSON API - 较慢,≤500 LED", - "targets.protocol.serial": "串口", - "search.open": "搜索 (Ctrl+K)", - "search.placeholder": "搜索实体... (Ctrl+K)", - "search.loading": "加载中...", - "search.no_results": "未找到结果", - "search.group.devices": "设备", - "search.group.targets": "LED 目标", - "search.group.kc_targets": "关键颜色目标", - "search.group.css": "色带源", - "search.group.automations": "自动化", - "search.group.streams": "图片流", - "search.group.capture_templates": "采集模板", - "search.group.pp_templates": "后处理模板", - "search.group.pattern_templates": "图案模板", - "search.group.audio": "音频源", - "search.group.value": "值源", - "search.group.scenes": "场景预设", - "search.group.cspt": "色带处理模板", - "search.group.sync_clocks": "同步时钟", - "search.group.actions": "操作", - "search.action.start": "启动", - "search.action.stop": "停止", - "search.action.activate": "激活", - "search.action.enable": "启用", - "search.action.disable": "禁用", - "settings.backup.label": "备份配置", - "settings.backup.hint": "将所有配置(设备、目标、流、模板、自动化)下载为单个 JSON 文件。", - "settings.backup.button": "下载备份", - "settings.backup.success": "备份下载成功", - "settings.backup.error": "备份下载失败", - "settings.restore.label": "恢复配置", - "settings.restore.hint": "上传之前下载的备份文件以替换所有配置。服务器将自动重启。", - "settings.restore.button": "从备份恢复", - "settings.restore.confirm": "这将替换所有配置并重启服务器。确定继续吗?", - "settings.restore.success": "配置已恢复", - "settings.restore.error": "恢复失败", - "settings.restore.restarting": "服务器正在重启...", - "settings.restore.restart_timeout": "服务器未响应。请手动刷新页面。", - "settings.restart_server": "重启服务器", - "settings.restart_confirm": "重启服务器?活跃的目标将被停止。", - "settings.restarting": "正在重启服务器...", - "settings.button.close": "关闭", - "settings.log_level.label": "日志级别", - "settings.log_level.hint": "实时更改服务器日志详细程度。DEBUG 显示最多细节;CRITICAL 仅显示致命错误。", - "settings.log_level.save": "应用", - "settings.log_level.saved": "日志级别已更改", - "settings.log_level.save_error": "更改日志级别失败", - "settings.log_level.desc.debug": "详细开发输出", - "settings.log_level.desc.info": "正常运行消息", - "settings.log_level.desc.warning": "潜在问题", - "settings.log_level.desc.error": "仅显示错误", - "settings.log_level.desc.critical": "仅显示致命错误", - "settings.auto_backup.label": "自动备份", - "settings.auto_backup.hint": "自动定期创建所有配置的备份。当达到最大数量时,旧备份会被自动清理。", - "settings.auto_backup.enable": "启用自动备份", - "settings.auto_backup.interval_label": "间隔", - "settings.auto_backup.max_label": "最大备份数", - "settings.auto_backup.save": "保存设置", - "settings.auto_backup.saved": "自动备份设置已保存", - "settings.auto_backup.save_error": "保存自动备份设置失败", - "settings.auto_backup.backup_now": "立即备份", - "settings.auto_backup.backup_created": "备份已创建", - "settings.auto_backup.backup_error": "备份失败", - "settings.auto_backup.last_backup": "上次备份", - "settings.auto_backup.never": "从未", - "settings.saved_backups.label": "已保存的备份", - "settings.saved_backups.hint": "存储在服务器上的自动备份文件。下载到本地保存,或删除以释放空间。", - "settings.saved_backups.empty": "没有已保存的备份", - "settings.saved_backups.restore": "恢复", - "settings.saved_backups.download": "下载", - "settings.saved_backups.delete": "删除", - "settings.saved_backups.delete_confirm": "删除此备份文件?", - "settings.saved_backups.delete_error": "删除备份失败", - "settings.saved_backups.type.auto": "自动", - "settings.saved_backups.type.manual": "手动", - "settings.mqtt.label": "MQTT", - "settings.mqtt.hint": "配置 MQTT 代理连接,用于自动化条件和触发器。", - "settings.mqtt.enabled": "启用 MQTT", - "settings.mqtt.host_label": "代理主机", - "settings.mqtt.port_label": "端口", - "settings.mqtt.username_label": "用户名", - "settings.mqtt.password_label": "密码", - "settings.mqtt.password_set_hint": "已设置密码 — 留空以保留", - "settings.mqtt.client_id_label": "客户端 ID", - "settings.mqtt.base_topic_label": "基础主题", - "settings.mqtt.save": "保存 MQTT 设置", - "settings.mqtt.saved": "MQTT 设置已保存", - "settings.mqtt.save_error": "保存 MQTT 设置失败", - "settings.mqtt.error_host_required": "代理主机不能为空", - "settings.logs.label": "服务器日志", - "settings.logs.hint": "实时查看服务器日志。使用过滤器显示所需的日志级别。", - "settings.logs.connect": "连接", - "settings.logs.disconnect": "断开", - "settings.logs.clear": "清除", - "settings.logs.error": "日志查看器连接失败", - "settings.logs.filter.all": "所有级别", - "settings.logs.filter.info": "Info+", - "settings.logs.filter.warning": "Warning+", - "settings.logs.filter.error": "仅错误", - "settings.logs.filter.all_desc": "显示所有日志消息", - "settings.logs.filter.info_desc": "Info、警告和错误", - "settings.logs.filter.warning_desc": "仅警告和错误", - "settings.logs.filter.error_desc": "仅错误", - "device.error.power_off_failed": "关闭设备失败", - "device.error.remove_failed": "移除设备失败", - "device.error.settings_load_failed": "加载设备设置失败", - "device.error.brightness": "更新亮度失败", - "device.error.required": "请填写所有字段", - "device.error.update": "更新设备失败", - "device.error.save": "保存设置失败", - "device.error.clone_failed": "克隆设备失败", - "device_discovery.error.fill_all_fields": "请填写所有字段", - "device_discovery.added": "设备添加成功", - "device_discovery.error.add_failed": "添加设备失败", - "calibration.error.load_failed": "加载校准失败", - "calibration.error.css_load_failed": "加载色带源失败", - "calibration.error.test_toggle_failed": "切换测试边缘失败", - "calibration.error.save_failed": "保存校准失败", - "calibration.error.led_count_mismatch": "LED总数必须等于设备LED数量", - "calibration.error.led_count_exceeded": "校准的LED超过了LED总数", - "calibration.mode.simple": "简单", - "calibration.mode.advanced": "高级", - "calibration.switch_to_advanced": "切换到高级模式", - "calibration.advanced.title": "高级校准", - "calibration.advanced.switch_to_simple": "切换到简单模式", - "calibration.advanced.lines_title": "线段", - "calibration.advanced.canvas_hint": "拖动显示器重新排列。点击边缘选择线段。滚动缩放,拖动空白区域平移。", - "calibration.advanced.reset_view": "重置视图", - "calibration.advanced.line_properties": "线段属性", - "calibration.advanced.picture_source": "来源:", - "calibration.advanced.picture_source.hint": "此线段采样的图片来源(显示器)", - "calibration.advanced.edge": "边缘:", - "calibration.advanced.edge.hint": "从屏幕哪条边缘采样像素", - "calibration.advanced.led_count": "LED数:", - "calibration.advanced.led_count.hint": "映射到此线段的LED数量", - "calibration.advanced.span_start": "起始位置:", - "calibration.advanced.span_start.hint": "沿边缘开始采样的位置(0 = 起点,1 = 终点)。用于仅覆盖边缘的一部分。", - "calibration.advanced.span_end": "结束位置:", - "calibration.advanced.span_end.hint": "沿边缘结束采样的位置(0 = 起点,1 = 终点)。与起始位置一起定义活动区域。", - "calibration.advanced.border_width": "深度(像素):", - "calibration.advanced.border_width.hint": "从边缘向内采样多少像素。较大的值会捕获更多屏幕内部区域。", - "calibration.advanced.reverse": "反转", - "calibration.advanced.no_lines_warning": "请至少添加一条线段", - "dashboard.error.automation_toggle_failed": "切换自动化失败", - "dashboard.error.start_failed": "启动处理失败", - "dashboard.error.stop_failed": "停止处理失败", - "dashboard.error.stop_all": "停止所有目标失败", - "target.error.editor_open_failed": "打开目标编辑器失败", - "target.error.start_failed": "启动目标失败", - "target.error.stop_failed": "停止目标失败", - "target.error.clone_failed": "克隆目标失败", - "target.error.delete_failed": "删除目标失败", - "targets.stop_all.button": "全部停止", - "targets.stop_all.none_running": "当前没有运行中的目标", - "targets.stop_all.stopped": "已停止 {count} 个目标", - "targets.stop_all.error": "停止目标失败", - "audio_source.error.load": "加载音频源失败", - "audio_template.error.clone_failed": "克隆音频模板失败", - "value_source.error.load": "加载数值源失败", - "color_strip.error.editor_open_failed": "打开色带编辑器失败", - "color_strip.error.clone_failed": "克隆色带源失败", - "color_strip.error.delete_failed": "删除色带源失败", - "pattern.error.editor_open_failed": "打开图案模板编辑器失败", - "pattern.error.clone_failed": "克隆图案模板失败", - "pattern.error.delete_failed": "删除图案模板失败", - "pattern.error.capture_bg_failed": "捕获背景失败", - "stream.error.clone_picture_failed": "克隆图片源失败", - "stream.error.clone_capture_failed": "克隆捕获模板失败", - "stream.error.clone_pp_failed": "克隆后处理模板失败", - "kc_target.error.editor_open_failed": "打开关键颜色编辑器失败", - "kc_target.error.clone_failed": "克隆关键颜色目标失败", - "kc_target.error.delete_failed": "删除关键颜色目标失败", - "theme.switched.dark": "已切换到深色主题", - "theme.switched.light": "已切换到浅色主题", - "accent.color.updated": "强调色已更新", - "search.footer": "↑↓ 导航 · Enter 选择 · Esc 关闭", - "sync_clock.group.title": "同步时钟", - "sync_clock.add": "添加同步时钟", - "sync_clock.edit": "编辑同步时钟", - "sync_clock.name": "名称:", - "sync_clock.name.placeholder": "主动画时钟", - "sync_clock.name.hint": "此同步时钟的描述性名称", - "sync_clock.speed": "速度:", - "sync_clock.speed.hint": "所有关联源的动画速度倍率。1.0 = 正常,2.0 = 双倍,0.5 = 半速。", - "sync_clock.description": "描述(可选):", - "sync_clock.description.placeholder": "可选描述", - "sync_clock.description.hint": "关于此时钟用途的可选备注", - "sync_clock.status.running": "运行中", - "sync_clock.status.paused": "已暂停", - "sync_clock.action.pause": "暂停", - "sync_clock.action.resume": "恢复", - "sync_clock.action.reset": "重置", - "sync_clock.error.name_required": "时钟名称为必填项", - "sync_clock.error.load": "加载同步时钟失败", - "sync_clock.created": "同步时钟已创建", - "sync_clock.updated": "同步时钟已更新", - "sync_clock.deleted": "同步时钟已删除", - "sync_clock.paused": "时钟已暂停", - "sync_clock.resumed": "时钟已恢复", - "sync_clock.reset_done": "时钟已重置为零", - "sync_clock.delete.confirm": "删除此同步时钟?关联的源将失去同步并以默认速度运行。", - "sync_clock.elapsed": "已用时间", - "weather_source.group.title": "天气源", - "weather_source.add": "添加天气源", - "weather_source.edit": "编辑天气源", - "weather_source.name": "名称:", - "weather_source.name.placeholder": "我的天气", - "weather_source.name.hint": "天气数据源的描述性名称", - "weather_source.provider": "提供商:", - "weather_source.provider.hint": "天气数据提供商。Open-Meteo免费且无需API密钥。", - "weather_source.provider.open_meteo.desc": "免费,无需API密钥", - "weather_source.location": "位置:", - "weather_source.location.hint": "您的地理坐标。使用自动检测按钮或手动输入。", - "weather_source.latitude": "纬度:", - "weather_source.longitude": "经度:", - "weather_source.use_my_location": "使用我的位置", - "weather_source.update_interval": "更新间隔:", - "weather_source.update_interval.hint": "获取天气数据的频率。值越低,更新越及时。", - "weather_source.description": "描述(可选):", - "weather_source.description.placeholder": "可选描述", - "weather_source.test": "测试", - "weather_source.error.name_required": "天气源名称为必填项", - "weather_source.error.load": "加载天气源失败", - "weather_source.created": "天气源已创建", - "weather_source.updated": "天气源已更新", - "weather_source.deleted": "天气源已删除", - "weather_source.delete.confirm": "删除此天气源?关联的色带源将失去天气数据。", - "weather_source.geo.success": "位置已检测", - "weather_source.geo.error": "地理定位失败", - "weather_source.geo.not_supported": "您的浏览器不支持地理定位", - "streams.group.weather": "天气", - "section.empty.weather_sources": "暂无天气源。点击+添加。", - "color_strip.clock": "同步时钟:", - "color_strip.clock.hint": "关联同步时钟以在多个源之间同步动画。速度在时钟上控制。", - "graph.title": "图表", - "graph.fit_all": "显示所有节点", - "graph.zoom_in": "放大", - "graph.zoom_out": "缩小", - "graph.search": "搜索节点", - "graph.search_placeholder": "搜索实体...", - "graph.legend": "图例", - "graph.minimap": "小地图", - "graph.relayout": "重新布局", - "graph.empty": "暂无实体", - "graph.empty.hint": "创建设备、源和目标后即可在此查看。", - "graph.disconnect": "断开连接", - "graph.connection_updated": "连接已更新", - "graph.connection_failed": "更新连接失败", - "graph.connection_removed": "连接已移除", - "graph.disconnect_failed": "断开连接失败", - "graph.relayout_confirm": "重置所有手动节点位置并重新布局图表?", - "graph.fullscreen": "切换全屏", - "graph.add_entity": "添加实体", - "graph.color_picker": "节点颜色", - "graph.filter": "筛选节点", - "graph.filter_placeholder": "按名称筛选...", - "graph.filter_clear": "清除筛选", - "graph.filter_running": "运行中", - "graph.filter_stopped": "已停止", - "graph.filter_types": "类型", - "graph.filter_group.capture": "捕获", - "graph.filter_group.strip": "色带", - "graph.filter_group.audio": "音频", - "graph.filter_group.targets": "目标", - "graph.filter_group.other": "其他", - "graph.bulk_delete_confirm": "删除 {count} 个选中的实体?", - "graph.nothing_to_undo": "没有可撤销的操作", - "graph.nothing_to_redo": "没有可重做的操作", - "graph.help_title": "键盘快捷键", - "graph.help.search": "搜索", - "graph.help.filter": "筛选", - "graph.help.add": "添加实体", - "graph.help.shortcuts": "快捷键", - "graph.help.delete": "删除 / 断开", - "graph.help.select_all": "全选", - "graph.help.undo": "撤销", - "graph.help.redo": "重做", - "graph.help.fullscreen": "全屏", - "graph.help.deselect": "取消选择", - "graph.help.navigate": "节点导航", - "graph.help.click": "单击", - "graph.help.click_desc": "选择节点", - "graph.help.dblclick": "双击", - "graph.help.dblclick_desc": "缩放到节点", - "graph.help.shift_click": "Shift+单击", - "graph.help.shift_click_desc": "多选", - "graph.help.shift_drag": "Shift+拖拽", - "graph.help.shift_drag_desc": "框选", - "graph.help.drag_node": "拖拽节点", - "graph.help.drag_node_desc": "重新定位", - "graph.help.drag_port": "拖拽端口", - "graph.help.drag_port_desc": "连接实体", - "graph.help.right_click": "右键边线", - "graph.help.right_click_desc": "断开连接", - "graph.tooltip.fps": "帧率", - "graph.tooltip.errors": "错误", - "graph.tooltip.uptime": "运行时间", - "automation.enabled": "自动化已启用", - "automation.disabled": "自动化已禁用", - "scene_preset.activated": "预设已激活", - "scene_preset.used_by": "被 %d 个自动化使用", - "settings.api_keys.label": "API 密钥", - "settings.api_keys.hint": "API 密钥在服务器配置文件 (config.yaml) 中定义。编辑文件并重启服务器以应用更改。", - "settings.api_keys.empty": "未配置 API 密钥", - "settings.api_keys.load_error": "加载 API 密钥失败", - "settings.partial.label": "部分导出 / 导入", - "settings.partial.hint": "导出或导入单个实体类型。导入会替换或合并现有数据并重启服务器。", - "settings.partial.store.devices": "设备", - "settings.partial.store.output_targets": "LED 目标", - "settings.partial.store.color_strip_sources": "色带", - "settings.partial.store.picture_sources": "图像源", - "settings.partial.store.audio_sources": "音频源", - "settings.partial.store.audio_templates": "音频模板", - "settings.partial.store.capture_templates": "捕获模板", - "settings.partial.store.postprocessing_templates": "后处理模板", - "settings.partial.store.color_strip_processing_templates": "CSS 处理模板", - "settings.partial.store.pattern_templates": "图案模板", - "settings.partial.store.value_sources": "值源", - "settings.partial.store.sync_clocks": "同步时钟", - "settings.partial.store.automations": "自动化", - "settings.partial.store.scene_presets": "场景预设", - "settings.partial.export_button": "导出", - "settings.partial.import_button": "从文件导入", - "settings.partial.merge_label": "合并(添加/覆盖,保留现有)", - "settings.partial.export_success": "导出成功", - "settings.partial.export_error": "导出失败", - "settings.partial.import_success": "导入成功", - "settings.partial.import_error": "导入失败", - "settings.partial.import_confirm_replace": "这将替换所有 {store} 数据并重启服务器。继续吗?", - "settings.partial.import_confirm_merge": "这将合并 {store} 数据并重启服务器。继续吗?", - "section.empty.devices": "暂无设备。点击 + 添加。", - "section.empty.targets": "暂无 LED 目标。点击 + 添加。", - "section.empty.kc_targets": "暂无键色目标。点击 + 添加。", - "section.empty.pattern_templates": "暂无图案模板。点击 + 添加。", - "section.empty.picture_sources": "暂无源。点击 + 添加。", - "section.empty.capture_templates": "暂无捕获模板。点击 + 添加。", - "section.empty.pp_templates": "暂无后处理模板。点击 + 添加。", - "section.empty.audio_sources": "暂无音频源。点击 + 添加。", - "section.empty.audio_templates": "暂无音频模板。点击 + 添加。", - "section.empty.color_strips": "暂无色带。点击 + 添加。", - "section.empty.value_sources": "暂无值源。点击 + 添加。", - "section.empty.sync_clocks": "暂无同步时钟。点击 + 添加。", - "section.empty.cspt": "暂无 CSS 处理模板。点击 + 添加。", - "section.empty.automations": "暂无自动化。点击 + 添加。", - "section.empty.scenes": "暂无场景预设。点击 + 添加。", - "bulk.select": "选择", - "bulk.cancel": "取消", - "bulk.selected_count.one": "已选 {count} 项", - "bulk.selected_count.other": "已选 {count} 项", - "bulk.select_all": "全选", - "bulk.deselect_all": "取消全选", - "bulk.delete": "删除", - "bulk.start": "启动", - "bulk.stop": "停止", - "bulk.enable": "启用", - "bulk.disable": "禁用", - "bulk.confirm_delete.one": "删除 {count} 项?", - "bulk.confirm_delete.other": "删除 {count} 项?", - "appearance.style.label": "样式预设", - "appearance.style.hint": "选择一个视觉主题 — 字体和配色方案一起应用。", - "appearance.preset.default": "默认", - "appearance.preset.midnight": "午夜", - "appearance.preset.ember": "余烬", - "appearance.preset.arctic": "极地", - "appearance.preset.terminal": "终端", - "appearance.preset.neon": "霓虹", - "appearance.preset.sakura": "樱花", - "appearance.preset.ocean": "海洋", - "appearance.preset.copper": "铜色", - "appearance.preset.vapor": "蒸汽波", - "appearance.preset.monolith": "黑白", - "appearance.preset.applied": "样式已应用", - "appearance.bg.label": "背景效果", - "appearance.bg.hint": "在界面后面添加环境背景层。", - "appearance.bg.none": "无", - "appearance.bg.noise": "噪声场", - "appearance.bg.aurora": "极光", - "appearance.bg.plasma": "等离子", - "appearance.bg.rain": "数字雨", - "appearance.bg.stars": "星空", - "appearance.bg.warp": "隧道", - "appearance.bg.grid": "点阵", - "appearance.bg.mesh": "渐变网格", - "appearance.bg.scanlines": "扫描线", - "appearance.bg.applied": "背景效果已应用", - - "settings.tab.updates": "更新", - "settings.tab.about": "关于", - "update.status_label": "更新状态", - "update.current_version": "当前版本:", - "update.badge_tooltip": "有新版本可用 — 点击查看详情", - "update.available": "版本 {version} 可用", - "update.up_to_date": "已是最新版本", - "update.prerelease": "预发布", - "update.view_release": "查看发布", - "update.dismiss": "忽略", - "update.check_now": "检查更新", - "update.check_error": "检查更新失败", - "update.last_check": "上次检查", - "update.never": "从未", - "update.release_notes": "发布说明", - "update.view_release_notes": "查看发布说明", - "update.auto_check_label": "自动检查设置", - "update.auto_check_hint": "在后台定期检查新版本。", - "update.enable": "启用自动检查", - "update.interval_label": "检查间隔", - "update.channel_label": "频道", - "update.channel.stable": "稳定版", - "update.channel.stable_desc": "仅稳定版本", - "update.channel.prerelease": "预发布", - "update.channel.prerelease_desc": "包括 alpha、beta 和 RC 版本", - "update.save_settings": "保存设置", - "update.settings_saved": "更新设置已保存", - "update.settings_save_error": "保存更新设置失败", - "update.apply_now": "立即更新", - "update.apply_confirm": "下载并安装版本 {version}?服务器将自动重启。", - "update.apply_error": "更新失败", - "update.applying": "正在应用更新…", - "update.downloading": "正在下载…", - "update.install_type_label": "安装类型:", - "update.install_type.installer": "Windows 安装程序", - "update.install_type.portable": "便携版", - "update.install_type.docker": "Docker", - "update.install_type.dev": "开发环境", - - "color_strip.notification.search_apps": "搜索通知应用…", - - "asset.group.title": "资源", - "asset.upload": "上传资源", - "asset.edit": "编辑资源", - "asset.name": "名称:", - "asset.name.hint": "资源的显示名称。", - "asset.description": "描述:", - "asset.description.hint": "资源的可选描述。", - "asset.file": "文件:", - "asset.file.hint": "选择要上传的文件(声音、图片、视频或其他)。", - "asset.drop_or_browse": "拖放文件到此处或点击浏览", - "asset.uploaded": "资源已上传", - "asset.updated": "资源已更新", - "asset.deleted": "资源已删除", - "asset.confirm_delete": "删除此资源?", - "asset.error.name_required": "名称为必填项", - "asset.error.no_file": "请选择要上传的文件", - "asset.error.delete_failed": "删除资源失败", - "asset.error.play_failed": "播放声音失败", - "asset.error.download_failed": "下载资源失败", - "asset.play": "播放", - "asset.download": "下载", - "asset.prebuilt": "内置", - "asset.prebuilt_restored": "已恢复 {count} 个内置资源", - "asset.prebuilt_none_to_restore": "所有内置资源均已可用", - "asset.restore_prebuilt": "恢复内置声音", - "asset.type.sound": "声音", - "asset.type.image": "图片", - "asset.type.video": "视频", - "asset.type.other": "其他", - "streams.group.assets": "资源", - "section.empty.assets": "暂无资源。点击 + 上传一个。", - - "donation.message": "LedGrab 是免费开源软件。如果它对您有帮助,请考虑支持开发。", - "donation.support": "支持项目", - "donation.view_source": "查看源代码", - "donation.later": "稍后提醒", - "donation.dismiss": "不再显示", - "donation.about_title": "关于 LedGrab", - "donation.about_opensource": "LedGrab 是开源软件,可免费使用和修改。", - "donation.about_donate": "支持开发", - "donation.about_license": "MIT 许可证" -} \ No newline at end of file + "app.title": "LED Grab", + "app.version": "版本:", + "app.api_docs": "API 文档", + "app.connection_lost": "服务器不可达", + "app.connection_retrying": "正在尝试重新连接…", + "app.server_restarting": "服务器正在重启…", + "app.server_restarting_sub": "请稍候,服务器即将恢复。", + "demo.badge": "演示", + "demo.banner": "您正处于演示模式 — 所有设备和数据均为虚拟。未使用任何真实硬件。", + "theme.toggle": "切换主题", + "bg.anim.toggle": "切换动态背景", + "accent.title": "主题色", + "accent.custom": "自定义", + "accent.reset": "重置", + "locale.change": "切换语言", + "auth.login": "登录", + "auth.logout": "退出", + "auth.authenticated": "● 已认证", + "auth.title": "登录 LED Grab", + "auth.message": "请输入 API 密钥以进行身份验证并访问 LED Grab。", + "auth.label": "API 密钥:", + "auth.placeholder": "输入您的 API 密钥...", + "auth.hint": "API 密钥将安全存储在浏览器的本地存储中。", + "auth.button.cancel": "取消", + "auth.button.login": "登录", + "auth.error.required": "请输入 API 密钥", + "auth.success": "登录成功!", + "auth.logout.confirm": "确定要退出登录吗?", + "auth.logout.success": "已成功退出", + "auth.please_login": "请先登录", + "auth.session_expired": "会话已过期或 API 密钥无效,请重新登录。", + "auth.toggle_password": "切换密码可见性", + "auth.prompt_enter": "Enter your API key:", + "auth.prompt_update": "Current API key is set. Enter new key to update or leave blank to remove:", + "api_key.login": "登录", + "displays.title": "可用显示器", + "displays.layout": "显示器", + "displays.information": "显示器信息", + "displays.legend.primary": "主显示器", + "displays.legend.secondary": "副显示器", + "displays.badge.primary": "主", + "displays.badge.secondary": "副", + "displays.resolution": "分辨率:", + "displays.refresh_rate": "刷新率:", + "displays.position": "位置:", + "displays.index": "显示器序号:", + "displays.loading": "正在加载显示器...", + "displays.none": "没有可用的显示器", + "displays.failed": "加载显示器失败", + "displays.picker.title": "选择显示器", + "displays.picker.title.device": "选择设备", + "displays.picker.select": "选择显示器...", + "displays.picker.click_to_select": "点击选择此显示器", + "displays.picker.adb_connect": "连接 ADB 设备", + "displays.picker.adb_connect.placeholder": "IP 地址(例如 192.168.2.201)", + "displays.picker.adb_connect.button": "连接", + "displays.picker.adb_connect.success": "设备已连接", + "displays.picker.adb_connect.error": "连接设备失败", + "displays.picker.adb_disconnect": "断开连接", + "displays.picker.no_android": "未找到 Android 设备。请通过 USB 连接或在上方输入 IP 地址。", + "templates.title": "引擎模板", + "templates.description": "采集模板定义屏幕的采集方式。每个模板使用特定的采集引擎(MSS、DXcam、WGC)及自定义设置。将模板分配给设备以获得最佳性能。", + "templates.loading": "正在加载模板...", + "templates.empty": "尚未配置采集模板", + "templates.add": "添加引擎模板", + "templates.edit": "编辑引擎模板", + "templates.name": "模板名称:", + "templates.name.placeholder": "我的自定义模板", + "templates.description.label": "描述(可选):", + "templates.description.placeholder": "描述此模板...", + "templates.engine": "采集引擎:", + "templates.engine.hint": "选择要使用的屏幕采集技术", + "templates.engine.select": "选择引擎...", + "templates.engine.unavailable": "不可用", + "templates.engine.unavailable.hint": "此引擎在您的系统上不可用", + "templates.engine.mss.desc": "跨平台,纯Python", + "templates.engine.dxcam.desc": "DirectX,低延迟", + "templates.engine.bettercam.desc": "DirectX,高性能", + "templates.engine.camera.desc": "USB/IP摄像头捕获", + "templates.engine.scrcpy.desc": "Android屏幕镜像", + "templates.engine.wgc.desc": "Windows图形捕获", + "templates.config": "配置", + "templates.config.show": "显示配置", + "templates.config.none": "无额外配置", + "templates.config.default": "默认", + "templates.config.camera_backend.auto": "自动检测最佳后端", + "templates.config.camera_backend.dshow": "Windows DirectShow", + "templates.config.camera_backend.msmf": "Windows Media Foundation", + "templates.config.camera_backend.v4l2": "Linux Video4Linux2", + "templates.created": "模板创建成功", + "templates.updated": "模板更新成功", + "templates.deleted": "模板删除成功", + "templates.delete.confirm": "确定要删除此模板吗?", + "templates.error.load": "加载模板失败", + "templates.error.engines": "加载引擎失败", + "templates.error.required": "请填写所有必填项", + "templates.error.delete": "删除模板失败", + "templates.test.title": "测试采集", + "templates.test.description": "保存前测试此模板,查看采集预览和性能指标。", + "templates.test.display": "显示器:", + "templates.test.display.select": "选择显示器...", + "templates.test.duration": "采集时长(秒):", + "templates.test.border_width": "边框宽度(像素):", + "templates.test.run": "运行", + "templates.test.running": "正在运行测试...", + "templates.test.results.preview": "全幅采集预览", + "templates.test.results.borders": "边框提取", + "templates.test.results.top": "上", + "templates.test.results.right": "右", + "templates.test.results.bottom": "下", + "templates.test.results.left": "左", + "templates.test.results.performance": "性能", + "templates.test.results.capture_time": "采集", + "templates.test.results.extraction_time": "提取", + "templates.test.results.total_time": "总计", + "templates.test.results.max_fps": "最大 FPS", + "templates.test.results.duration": "时长", + "templates.test.results.frame_count": "帧数", + "templates.test.results.actual_fps": "实际 FPS", + "templates.test.results.avg_capture_time": "平均采集", + "templates.test.results.resolution": "分辨率:", + "templates.test.error.no_engine": "请选择采集引擎", + "templates.test.error.no_display": "请选择显示器", + "templates.test.error.failed": "测试失败", + "devices.title": "设备", + "device.select_type": "选择设备类型", + "devices.add": "添加新设备", + "devices.loading": "正在加载设备...", + "devices.none": "尚未配置设备", + "devices.failed": "加载设备失败", + "devices.wled_config": "WLED 配置:", + "devices.wled_note": "使用以下方式配置您的 WLED 设备(效果、分段、颜色顺序、功率限制等):", + "devices.wled_link": "官方 WLED 应用", + "devices.wled_note_or": "或内置的", + "devices.wled_webui_link": "WLED Web UI", + "devices.wled_note_webui": "(在浏览器中打开设备 IP 地址)。", + "devices.wled_note2": "此控制器发送像素颜色数据并控制每个设备的亮度。", + "device.scan": "自动发现", + "device.scan.empty": "未找到设备", + "device.scan.error": "网络扫描失败", + "device.scan.already_added": "已添加", + "device.scan.selected": "设备已选择", + "device.type": "设备类型:", + "device.type.hint": "选择 LED 控制器的类型", + "device.type.wled": "WLED", + "device.type.wled.desc": "通过HTTP/UDP控制的WiFi LED", + "device.type.adalight": "Adalight", + "device.type.adalight.desc": "Arduino串口LED协议", + "device.type.ambiled": "AmbiLED", + "device.type.ambiled.desc": "AmbiLED串口协议", + "device.type.mqtt": "MQTT", + "device.type.mqtt.desc": "通过MQTT代理发布LED数据", + "device.type.ws": "WebSocket", + "device.type.ws.desc": "通过WebSocket流式传输LED数据", + "device.type.openrgb": "OpenRGB", + "device.type.openrgb.desc": "通过OpenRGB控制RGB外设", + "device.type.dmx": "DMX", + "device.type.dmx.desc": "Art-Net / sACN (E1.31) 舞台灯光", + "device.type.mock": "Mock", + "device.type.mock.desc": "用于测试的虚拟设备", + "device.type.espnow": "ESP-NOW", + "device.type.espnow.desc": "Ultra-low-latency via ESP32 gateway", + "device.type.hue": "Philips Hue", + "device.type.hue.desc": "Hue Entertainment API streaming", + "device.type.usbhid": "USB HID", + "device.type.usbhid.desc": "USB RGB peripherals (keyboards, mice)", + "device.type.spi": "SPI Direct", + "device.type.spi.desc": "Raspberry Pi GPIO/SPI LED strips", + "device.type.chroma": "Razer Chroma", + "device.type.chroma.desc": "Razer peripherals via Chroma SDK", + "device.type.gamesense": "SteelSeries", + "device.type.gamesense.desc": "SteelSeries peripherals via GameSense", + "device.chroma.device_type": "Peripheral Type:", + "device.chroma.device_type.hint": "Which Razer peripheral to control via Chroma SDK", + "device.gamesense.device_type": "Peripheral Type:", + "device.gamesense.device_type.hint": "Which SteelSeries peripheral to control via GameSense", + "device.espnow.peer_mac": "Peer MAC:", + "device.espnow.peer_mac.hint": "MAC address of the remote ESP32 receiver (e.g. AA:BB:CC:DD:EE:FF)", + "device.espnow.channel": "WiFi Channel:", + "device.espnow.channel.hint": "WiFi channel (1-14). Must match the receiver's channel.", + "device.hue.url": "Bridge IP:", + "device.hue.url.hint": "IP address of your Hue bridge", + "device.hue.username": "Bridge Username:", + "device.hue.username.hint": "Hue bridge application key from pairing", + "device.hue.client_key": "Client Key:", + "device.hue.client_key.hint": "Entertainment API client key (hex string from pairing)", + "device.hue.group_id": "Entertainment Group:", + "device.hue.group_id.hint": "Entertainment configuration ID from your Hue bridge", + "device.usbhid.url": "VID:PID:", + "device.usbhid.url.hint": "USB Vendor:Product ID in hex (e.g. 1532:0084)", + "device.spi.url": "GPIO/SPI Path:", + "device.spi.url.hint": "GPIO pin or SPI device path (e.g. spi://gpio:18)", + "device.spi.speed": "SPI Speed (Hz):", + "device.spi.speed.hint": "SPI clock speed. 800000 Hz for WS2812, 2400000 Hz for APA102.", + "device.spi.led_type": "LED Chipset:", + "device.spi.led_type.hint": "Type of addressable LED strip connected to the GPIO/SPI pin", + "device.spi.led_type.ws2812b.desc": "Most common, 800 KHz data, 3-wire RGB", + "device.spi.led_type.ws2812.desc": "Original WS2812, 800 KHz, 3-wire RGB", + "device.spi.led_type.ws2811.desc": "External driver IC, 400 KHz, 12V strips", + "device.spi.led_type.sk6812.desc": "Samsung LED, 800 KHz, 3-wire RGB", + "device.spi.led_type.sk6812_rgbw.desc": "SK6812 with dedicated white channel", + "device.gamesense.peripheral.keyboard": "Keyboard", + "device.gamesense.peripheral.keyboard.desc": "Per-key RGB illumination", + "device.gamesense.peripheral.mouse": "Mouse", + "device.gamesense.peripheral.mouse.desc": "Mouse RGB zones", + "device.gamesense.peripheral.headset": "Headset", + "device.gamesense.peripheral.headset.desc": "Headset earcup lighting", + "device.gamesense.peripheral.mousepad": "Mousepad", + "device.gamesense.peripheral.mousepad.desc": "Mousepad edge lighting zones", + "device.gamesense.peripheral.indicator": "Indicator", + "device.gamesense.peripheral.indicator.desc": "OLED/LED status indicator", + "device.dmx_protocol": "DMX 协议:", + "device.dmx_protocol.hint": "Art-Net 使用 UDP 端口 6454,sACN (E1.31) 使用 UDP 端口 5568", + "device.dmx_protocol.artnet.desc": "UDP 单播,端口 6454", + "device.dmx_protocol.sacn.desc": "组播/单播,端口 5568", + "device.dmx_start_universe": "起始 Universe:", + "device.dmx_start_universe.hint": "第一个 DMX universe (0-32767)。超过 170 个 LED 时自动使用多个 universe。", + "device.dmx_start_channel": "起始通道:", + "device.dmx_start_channel.hint": "universe 中的第一个 DMX 通道 (1-512)", + "device.dmx.url": "IP 地址:", + "device.dmx.url.hint": "DMX 节点的 IP 地址(例如 192.168.1.50)", + "device.dmx.url.placeholder": "192.168.1.50", + "device.serial_port": "串口:", + "device.serial_port.hint": "选择 Adalight 设备的 COM 端口", + "device.serial_port.none": "未找到串口", + "device.serial_port.select": "选择端口...", + "device.led_count_manual.hint": "灯带上的 LED 数量(必须与 Arduino 程序匹配)", + "device.baud_rate": "波特率:", + "device.baud_rate.hint": "串口通信速率。越高 FPS 越高,但需要与 Arduino 程序匹配。", + "device.led_type": "LED 类型:", + "device.led_type.hint": "RGB(3通道)或 RGBW(4通道,带独立白色)", + "device.send_latency": "发送延迟(毫秒):", + "device.send_latency.hint": "每帧模拟网络/串口延迟(毫秒)", + "device.css_processing_template": "色带处理模板:", + "device.css_processing_template.hint": "应用于此设备所有色带输出的默认处理模板", + "device.mqtt_topic": "MQTT 主题:", + "device.mqtt_topic.hint": "用于发布像素数据的 MQTT 主题路径(例如 mqtt://ledgrab/device/name)", + "device.mqtt_topic.placeholder": "mqtt://ledgrab/device/客厅", + "device.ws_url": "连接 URL:", + "device.ws_url.hint": "客户端连接并接收 LED 数据的 WebSocket URL", + "device.openrgb.url": "OpenRGB URL:", + "device.openrgb.url.hint": "OpenRGB 服务器地址(例如 openrgb://localhost:6742/0)", + "device.openrgb.zone": "区域:", + "device.openrgb.zone.hint": "选择要控制的 LED 区域(全部不选则控制所有区域)", + "device.openrgb.zone.loading": "加载区域中…", + "device.openrgb.zone.error": "加载区域失败", + "device.openrgb.mode": "区域模式:", + "device.openrgb.mode.hint": "合并模式将所有区域作为一条连续 LED 灯带。独立模式让每个区域独立渲染完整效果。", + "device.openrgb.mode.combined": "合并灯带", + "device.openrgb.mode.separate": "独立区域", + "device.openrgb.added_multiple": "已添加 {count} 个设备", + "device.url.hint": "设备的 IP 地址或主机名(例如 http://192.168.1.100)", + "device.name": "设备名称:", + "device.name.placeholder": "客厅电视", + "device.url": "地址:", + "device.url.placeholder": "http://192.168.1.100", + "device.led_count": "LED 数量:", + "device.led_count.hint": "设备中配置的 LED 数量", + "device.led_count.hint.auto": "从设备自动检测", + "device.button.add": "添加设备", + "device.button.start": "启动", + "device.button.stop": "停止", + "device.button.settings": "常规设置", + "device.button.capture_settings": "采集设置", + "device.button.calibrate": "校准", + "device.button.remove": "移除", + "device.button.webui": "打开设备 Web UI", + "device.button.power_off": "关闭", + "device.button.ping": "Ping 设备", + "device.ping.online": "在线 ({ms}ms)", + "device.ping.offline": "设备离线", + "device.ping.error": "Ping 失败", + "device.power.off_success": "设备已关闭", + "device.status.connected": "已连接", + "device.status.disconnected": "已断开", + "device.status.error": "错误", + "device.status.processing": "处理中", + "device.status.idle": "空闲", + "device.fps": "FPS:", + "device.display": "显示器:", + "device.remove.confirm": "确定要移除此设备吗?", + "device.added": "设备添加成功", + "device.removed": "设备已移除", + "device.started": "处理已启动", + "device.stopped": "处理已停止", + "device.metrics.actual_fps": "实际 FPS", + "device.metrics.current_fps": "当前 FPS", + "device.metrics.target_fps": "目标 FPS", + "device.metrics.potential_fps": "潜在 FPS", + "device.metrics.frames": "帧数", + "device.metrics.frames_skipped": "已跳过", + "device.metrics.keepalive": "心跳", + "device.metrics.errors": "错误", + "device.metrics.uptime": "运行时长", + "device.metrics.timing": "管线时序:", + "device.metrics.device_fps": "设备刷新率", + "device.health.online": "在线", + "device.health.offline": "离线", + "device.health.streaming_unreachable": "流传输期间不可达", + "device.health.checking": "检测中...", + "device.last_seen.label": "最近检测", + "device.last_seen.just_now": "刚刚", + "device.last_seen.seconds": "%d秒前", + "device.last_seen.minutes": "%d分钟前", + "device.last_seen.hours": "%d小时前", + "device.last_seen.days": "%d天前", + "device.tutorial.start": "开始教程", + "device.tip.metadata": "设备信息(LED 数量、类型、颜色通道)从设备自动检测", + "device.tip.brightness": "滑动调节设备亮度", + "device.tip.start": "启动或停止屏幕采集处理", + "device.tip.settings": "配置设备常规设置(名称、地址、健康检查)", + "device.tip.capture_settings": "配置采集设置(显示器、采集模板)", + "device.tip.calibrate": "校准 LED 位置、方向和覆盖范围", + "device.tip.webui": "打开设备内置的 Web 界面进行高级配置", + "device.tip.add": "点击此处添加新的 LED 设备", + "settings.title": "设置", + "settings.tab.general": "常规", + "settings.tab.backup": "备份", + "settings.tab.mqtt": "MQTT", + "settings.tab.appearance": "外观", + "settings.logs.open_viewer": "打开日志查看器", + "settings.external_url.label": "外部 URL", + "settings.external_url.hint": "设置后,此基础 URL 将用于 webhook 链接和其他用户可见的链接,代替自动检测的本地 IP。示例:https://myserver.example.com:8080", + "settings.external_url.placeholder": "https://myserver.example.com:8080", + "settings.external_url.save": "保存", + "settings.external_url.saved": "外部 URL 已保存", + "settings.external_url.save_error": "保存外部 URL 失败", + "settings.general.title": "常规设置", + "settings.capture.title": "采集设置", + "settings.capture.saved": "采集设置已更新", + "settings.capture.failed": "保存采集设置失败", + "settings.brightness": "亮度:", + "settings.brightness.hint": "此设备的全局亮度(0-100%)", + "settings.url.hint": "设备的 IP 地址或主机名", + "settings.display_index": "显示器:", + "settings.display_index.hint": "为此设备采集哪个屏幕", + "settings.fps": "目标 FPS:", + "settings.fps.hint": "目标帧率(10-90)", + "settings.capture_template": "引擎模板:", + "settings.capture_template.hint": "此设备的屏幕采集引擎和配置", + "settings.button.cancel": "取消", + "settings.health_interval": "健康检查间隔(秒):", + "settings.health_interval.hint": "检查设备状态的频率(5-600秒)", + "settings.auto_shutdown": "自动恢复:", + "settings.auto_shutdown.hint": "当目标停止或服务器关闭时恢复设备到空闲状态", + "settings.button.save": "保存更改", + "settings.saved": "设置保存成功", + "settings.failed": "保存设置失败", + "calibration.title": "LED 校准", + "calibration.tip.led_count": "输入每条边的 LED 数量", + "calibration.tip.start_corner": "点击角落设置起始位置", + "calibration.tip.direction": "切换灯带方向(顺时针/逆时针)", + "calibration.tip.offset": "设置 LED 偏移 — 从 LED 0 到起始角落的距离", + "calibration.tip.span": "拖动绿色条调整覆盖范围", + "calibration.tip.test": "点击边缘切换测试 LED", + "calibration.tip.overlay": "切换屏幕叠加层以查看显示器上的 LED 位置和编号", + "calibration.tip.toggle_inputs": "点击 LED 总数切换边缘输入", + "calibration.tip.border_width": "从屏幕边缘采样多少像素来确定 LED 颜色", + "calibration.tip.skip_leds_start": "跳过灯带起始端的 LED — 被跳过的 LED 保持关闭", + "calibration.tip.skip_leds_end": "跳过灯带末尾端的 LED — 被跳过的 LED 保持关闭", + "tour.welcome": "欢迎使用 LED Grab!快速导览将带您了解界面。使用方向键或按钮进行导航。", + "tour.dashboard": "仪表盘 — 实时查看运行中的目标、自动化和设备状态。", + "tour.targets": "目标 — 添加 WLED 设备,配置 LED 目标的捕获设置和校准。", + "tour.sources": "来源 — 管理捕获模板、图片来源、音频来源和色带。", + "tour.graph": "图表 — 所有实体及其连接的可视化概览。拖动端口进行连接,右键单击边线断开连接。", + "tour.automations": "自动化 — 通过时间、音频或数值条件自动切换场景。", + "tour.settings": "设置 — 备份和恢复配置,管理自动备份。", + "tour.api": "API 文档 — 基于 Swagger 的交互式 REST API 文档。", + "tour.search": "搜索 — 使用 Ctrl+K 快速查找并导航到任意实体。", + "tour.theme": "主题 — 在深色和浅色模式之间切换。", + "tour.accent": "主题色 — 自定义界面的强调颜色。", + "tour.language": "语言 — 选择您偏好的界面语言。", + "tour.restart": "重新开始导览", + "tour.dash.perf": "性能 — 实时 FPS 图表、延迟指标和轮询间隔控制。", + "tour.dash.running": "运行中的目标 — 实时流媒体指标和快速停止控制。", + "tour.dash.stopped": "已停止的目标 — 一键启动。", + "tour.dash.automations": "自动化 — 活动自动化状态和快速启用/禁用切换。", + "tour.tgt.led_tab": "LED 标签 — 标准 LED 灯带目标,包含设备和色带配置。", + "tour.tgt.devices": "设备 — 在网络中发现的 LED 控制器。", + "tour.tgt.css": "色带 — 定义屏幕区域如何映射到 LED 段。", + "tour.tgt.targets": "LED 目标 — 将设备、色带和捕获源组合进行流式传输。", + "tour.tgt.kc_tab": "Key Colors — 使用颜色匹配代替像素映射的替代目标类型。", + "tour.src.raw": "原始 — 来自显示器的实时屏幕捕获源。", + "tour.src.templates": "捕获模板 — 可复用的捕获配置(分辨率、FPS、裁剪)。", + "tour.src.static": "静态图片 — 使用图片文件测试您的设置。", + "tour.src.processed": "处理 — 应用后处理效果,如模糊、亮度或色彩校正。", + "tour.src.color_strip": "色带 — 定义屏幕区域如何映射到 LED 段。", + "tour.src.audio": "音频 — 分析麦克风或系统音频以实现响应式 LED 效果。", + "tour.src.value": "数值 — 用于自动化条件的数字数据源。", + "tour.src.sync": "同步时钟 — 在多个源之间同步动画的共享定时器。", + "tour.auto.list": "自动化 — 基于时间、音频或数值条件自动激活场景。", + "tour.auto.add": "点击 + 创建包含条件和要激活场景的新自动化。", + "tour.auto.card": "每张卡片显示自动化状态、条件和快速编辑/切换控制。", + "tour.auto.scenes_list": "场景 — 保存的系统状态,自动化可以激活或您可以手动应用。", + "tour.auto.scenes_add": "点击 + 将当前系统状态捕获为新的场景预设。", + "tour.auto.scenes_card": "每个场景卡片显示目标/设备数量。点击编辑、重新捕获或激活。", + "calibration.tutorial.start": "开始教程", + "calibration.overlay_toggle": "叠加层", + "calibration.start_position": "起始位置:", + "calibration.position.bottom_left": "左下", + "calibration.position.bottom_right": "右下", + "calibration.position.top_left": "左上", + "calibration.position.top_right": "右上", + "calibration.direction": "方向:", + "calibration.direction.clockwise": "顺时针", + "calibration.direction.counterclockwise": "逆时针", + "calibration.leds.top": "顶部 LED:", + "calibration.leds.right": "右侧 LED:", + "calibration.leds.bottom": "底部 LED:", + "calibration.leds.left": "左侧 LED:", + "calibration.offset": "LED 偏移:", + "calibration.offset.hint": "从物理 LED 0 到起始角落的距离(沿灯带方向)", + "calibration.skip_start": "跳过 LED(起始):", + "calibration.skip_start.hint": "灯带起始端关闭的 LED 数量(0 = 无)", + "calibration.skip_end": "跳过 LED(末尾):", + "calibration.skip_end.hint": "灯带末尾端关闭的 LED 数量(0 = 无)", + "calibration.border_width": "边框(像素):", + "calibration.border_width.hint": "从屏幕边缘采样多少像素来确定 LED 颜色(1-100)", + "calibration.button.cancel": "取消", + "calibration.button.save": "保存", + "calibration.saved": "校准已保存", + "calibration.failed": "保存校准失败", + "server.healthy": "服务器在线", + "server.offline": "服务器离线", + "error.unauthorized": "未授权 - 请先登录", + "error.network": "网络错误", + "error.unknown": "发生错误", + "modal.discard_changes": "有未保存的更改。是否放弃?", + "confirm.title": "确认操作", + "confirm.yes": "是", + "confirm.no": "否", + "confirm.stop_all": "停止所有运行中的目标?", + "confirm.turn_off_device": "关闭此设备?", + "common.loading": "加载中...", + "common.delete": "删除", + "common.remove": "移除", + "common.edit": "编辑", + "common.clone": "克隆", + "common.none": "无", + "common.none_no_cspt": "无(无处理模板)", + "common.none_no_input": "无(无输入源)", + "common.none_own_speed": "无(使用自身速度)", + "common.undo": "撤销", + "validation.required": "此字段为必填项", + "bulk.processing": "处理中…", + "api.error.timeout": "请求超时 — 请重试", + "api.error.network": "网络错误 — 请检查连接", + "palette.search": "搜索…", + "section.filter.placeholder": "筛选...", + "section.filter.reset": "清除筛选", + "tags.label": "标签", + "tags.hint": "为卡片分配标签以进行分组和筛选", + "tags.placeholder": "添加标签...", + "section.expand_all": "全部展开", + "section.collapse_all": "全部折叠", + "streams.title": "源", + "streams.description": "源定义采集管线。原始源使用采集模板从显示器采集。处理源对另一个源应用后处理。将源分配给设备。", + "streams.group.raw": "源", + "streams.group.raw_templates": "引擎模板", + "streams.group.processed": "源", + "streams.group.proc_templates": "滤镜模板", + "streams.group.css_processing": "处理模板", + "streams.group.color_strip": "色带源", + "streams.group.audio": "音频", + "streams.group.audio_templates": "音频模板", + "streams.section.streams": "源", + "streams.add": "添加源", + "streams.add.raw": "添加屏幕采集", + "streams.add.processed": "添加处理源", + "streams.edit": "编辑源", + "streams.edit.raw": "编辑屏幕采集", + "streams.edit.processed": "编辑处理源", + "streams.name": "源名称:", + "streams.name.placeholder": "我的源", + "streams.type": "类型:", + "streams.type.raw": "屏幕采集", + "streams.type.processed": "已处理", + "streams.display": "显示器:", + "streams.display.hint": "采集哪个屏幕", + "streams.capture_template": "引擎模板:", + "streams.capture_template.hint": "定义屏幕采集方式的引擎模板", + "streams.target_fps": "目标 FPS:", + "streams.target_fps.hint": "采集的目标帧率(1-90)", + "streams.source": "源:", + "streams.source.hint": "要应用处理滤镜的源", + "streams.pp_template": "滤镜模板:", + "streams.pp_template.hint": "要应用到源的滤镜模板", + "streams.description_label": "描述(可选):", + "streams.description_placeholder": "描述此源...", + "streams.created": "源创建成功", + "streams.updated": "源更新成功", + "streams.deleted": "源删除成功", + "streams.delete.confirm": "确定要删除此源吗?", + "streams.modal.loading": "加载中...", + "streams.error.load": "加载源失败", + "streams.error.required": "请填写所有必填项", + "streams.error.delete": "删除源失败", + "streams.test.title": "测试源", + "streams.test.run": "运行", + "streams.test.running": "正在测试源...", + "streams.test.duration": "采集时长(秒):", + "streams.test.error.failed": "源测试失败", + "postprocessing.title": "滤镜模板", + "postprocessing.description": "处理模板定义图像滤镜和色彩校正。将它们分配给处理图片源以实现跨设备的一致后处理。", + "postprocessing.add": "添加滤镜模板", + "postprocessing.edit": "编辑滤镜模板", + "postprocessing.name": "模板名称:", + "postprocessing.name.placeholder": "我的滤镜模板", + "filters.select_type": "选择滤镜类型...", + "filters.add": "添加滤镜", + "filters.remove": "移除", + "filters.drag_to_reorder": "拖动以重新排序", + "filters.empty": "尚未添加滤镜。使用下方选择器添加滤镜。", + "filters.brightness": "亮度", + "filters.brightness.desc": "调整整体图像亮度", + "filters.saturation": "饱和度", + "filters.saturation.desc": "增强或降低色彩强度", + "filters.gamma": "伽马", + "filters.gamma.desc": "非线性亮度曲线校正", + "filters.downscaler": "缩小", + "filters.downscaler.desc": "降低分辨率以加快处理", + "filters.pixelate": "像素化", + "filters.pixelate.desc": "马赛克式块平均", + "filters.auto_crop": "自动裁剪", + "filters.auto_crop.desc": "移除信箱式内容的黑边", + "filters.flip": "翻转", + "filters.flip.desc": "水平或垂直镜像翻转", + "filters.color_correction": "色彩校正", + "filters.color_correction.desc": "白平衡和色温调整", + "filters.filter_template": "滤镜模板", + "filters.filter_template.desc": "嵌入另一个处理模板", + "filters.css_filter_template": "色带滤镜模板", + "filters.css_filter_template.desc": "嵌入另一个色带处理模板", + "filters.frame_interpolation": "帧插值", + "filters.frame_interpolation.desc": "帧间混合以获得更平滑的输出", + "filters.noise_gate": "噪声门", + "filters.noise_gate.desc": "抑制低于阈值的细微色彩变化", + "filters.palette_quantization": "调色板量化", + "filters.palette_quantization.desc": "将颜色减少到有限调色板", + "filters.reverse": "反转", + "filters.reverse.desc": "反转色带中的LED顺序", + "postprocessing.description_label": "描述(可选):", + "postprocessing.description_placeholder": "描述此模板...", + "postprocessing.created": "模板创建成功", + "postprocessing.updated": "模板更新成功", + "postprocessing.deleted": "模板删除成功", + "postprocessing.delete.confirm": "确定要删除此滤镜模板吗?", + "postprocessing.error.load": "加载处理模板失败", + "postprocessing.error.required": "请填写所有必填项", + "postprocessing.error.delete": "删除处理模板失败", + "postprocessing.config.show": "显示设置", + "postprocessing.test.title": "测试滤镜模板", + "postprocessing.test.source_stream": "源:", + "postprocessing.test.running": "正在测试处理模板...", + "postprocessing.test.error.no_stream": "请选择一个源", + "postprocessing.test.error.failed": "处理模板测试失败", + "css_processing.title": "色带处理模板", + "css_processing.add": "添加色带处理模板", + "css_processing.edit": "编辑色带处理模板", + "css_processing.name": "模板名称:", + "css_processing.name_placeholder": "我的色带处理模板", + "css_processing.description_label": "描述(可选):", + "css_processing.description_placeholder": "描述此模板...", + "css_processing.created": "色带处理模板已创建", + "css_processing.updated": "色带处理模板已更新", + "css_processing.deleted": "色带处理模板已删除", + "css_processing.delete.confirm": "确定要删除此色带处理模板吗?", + "css_processing.error.required": "请填写所有必填字段", + "css_processing.error.load": "加载色带处理模板出错", + "css_processing.error.delete": "删除色带处理模板出错", + "css_processing.error.clone_failed": "克隆色带处理模板失败", + "device.button.stream_selector": "源设置", + "device.stream_settings.title": "源设置", + "device.stream_selector.label": "源:", + "device.stream_selector.hint": "选择一个源来定义此设备采集和处理的内容", + "device.stream_selector.none": "-- 未分配源 --", + "device.stream_selector.saved": "源设置已更新", + "device.stream_settings.border_width": "边框宽度(像素):", + "device.stream_settings.border_width_hint": "从屏幕边缘采样多少像素来确定 LED 颜色(1-100)", + "device.stream_settings.interpolation": "插值模式:", + "device.stream_settings.interpolation.average": "平均", + "device.stream_settings.interpolation.median": "中位数", + "device.stream_settings.interpolation.dominant": "主色", + "device.stream_settings.interpolation_hint": "如何从采样像素计算 LED 颜色", + "device.stream_settings.smoothing": "平滑:", + "device.stream_settings.smoothing_hint": "帧间时间混合(0=无,1=完全)。减少闪烁。", + "device.tip.stream_selector": "为此设备配置图片源和 LED 投影设置", + "streams.group.static_image": "静态图片", + "streams.add.static_image": "添加静态图片源", + "streams.edit.static_image": "编辑静态图片源", + "streams.type.static_image": "静态图片", + "streams.group.video": "视频", + "streams.add.video": "添加视频源", + "streams.edit.video": "编辑视频源", + "picture_source.type.video": "视频", + "picture_source.type.video.desc": "从上传的视频素材中流式传输帧", + "picture_source.video.loop": "循环:", + "picture_source.video.speed": "播放速度:", + "picture_source.video.start_time": "开始时间(秒):", + "picture_source.video.end_time": "结束时间(秒):", + "picture_source.video.resolution_limit": "最大宽度(像素):", + "picture_source.video.resolution_limit.hint": "解码时缩小视频以提高性能", + "streams.image_asset": "图片素材:", + "streams.image_asset.select": "选择图片素材…", + "streams.image_asset.search": "搜索图片素材…", + "streams.video_asset": "视频素材:", + "streams.video_asset.select": "选择视频素材…", + "streams.video_asset.search": "搜索视频素材…", + "targets.title": "目标", + "targets.description": "目标将色带源桥接到输出设备。每个目标引用一个设备和一个色带源。", + "targets.subtab.wled": "LED", + "targets.subtab.led": "LED", + "targets.section.devices": "设备", + "targets.section.color_strips": "色带源", + "targets.section.targets": "目标", + "targets.section.specific_settings": "特定设置", + "targets.section.advanced": "高级", + "targets.add": "添加目标", + "targets.edit": "编辑目标", + "targets.loading": "正在加载目标...", + "targets.none": "尚未配置目标", + "targets.failed": "加载目标失败", + "targets.name": "目标名称:", + "targets.name.placeholder": "我的目标", + "targets.device": "设备:", + "targets.device.hint": "选择要发送数据的 LED 设备", + "targets.device.none": "-- 选择设备 --", + "targets.color_strip_source": "色带源:", + "targets.color_strip_source.hint": "选择为此目标提供 LED 颜色的色带源", + "targets.no_css": "无源", + "targets.source": "源:", + "targets.source.hint": "要采集和处理的图片源", + "targets.source.none": "-- 未分配源 --", + "targets.metrics.pipeline": "管线详情", + "targets.fps": "目标 FPS:", + "targets.fps.hint": "采集和 LED 更新的目标帧率(1-90)", + "targets.fps.rec": "硬件最大 ≈ {fps} fps({leds} 个 LED)", + "targets.border_width": "边框宽度(像素):", + "targets.border_width.hint": "从屏幕边缘采样多少像素来确定 LED 颜色(1-100)", + "targets.interpolation": "插值模式:", + "targets.interpolation.hint": "如何从采样像素计算 LED 颜色", + "targets.interpolation.average": "平均", + "targets.interpolation.median": "中位数", + "targets.interpolation.dominant": "主色", + "targets.smoothing": "平滑:", + "targets.smoothing.hint": "帧间时间混合(0=无,1=完全)。减少闪烁。", + "targets.keepalive_interval": "心跳间隔:", + "targets.keepalive_interval.hint": "源静态时重新发送最后一帧的频率,保持设备在活动模式(0.5-5.0秒)", + "targets.created": "目标创建成功", + "targets.updated": "目标更新成功", + "targets.deleted": "目标删除成功", + "targets.delete.confirm": "确定要删除此目标吗?", + "targets.error.load": "加载目标失败", + "targets.error.required": "请填写所有必填项", + "targets.error.name_required": "请输入目标名称", + "targets.error.delete": "删除目标失败", + "targets.button.start": "启动", + "targets.button.stop": "停止", + "targets.status.processing": "处理中", + "targets.status.idle": "空闲", + "targets.status.error": "错误", + "targets.metrics.actual_fps": "实际 FPS", + "targets.metrics.target_fps": "目标 FPS", + "targets.metrics.frames": "帧数", + "targets.metrics.errors": "错误", + "targets.section.pattern_templates": "图案模板", + "pattern.add": "添加图案模板", + "pattern.edit": "编辑图案模板", + "pattern.name": "模板名称:", + "pattern.name.placeholder": "我的图案模板", + "pattern.description_label": "描述(可选):", + "pattern.description_placeholder": "描述此图案...", + "pattern.rectangles": "矩形", + "pattern.rect.name": "名称", + "pattern.rect.x": "X", + "pattern.rect.y": "Y", + "pattern.rect.width": "宽", + "pattern.rect.height": "高", + "pattern.rect.add": "添加矩形", + "pattern.rect.remove": "移除", + "pattern.rect.empty": "尚未定义矩形。请至少添加一个矩形。", + "pattern.created": "图案模板创建成功", + "pattern.updated": "图案模板更新成功", + "pattern.deleted": "图案模板删除成功", + "pattern.delete.confirm": "确定要删除此图案模板吗?", + "pattern.delete.referenced": "无法删除:此模板正被目标引用", + "pattern.error.required": "请填写所有必填项", + "pattern.visual_editor": "可视编辑器", + "pattern.capture_bg": "采集背景", + "pattern.source_for_bg": "背景源:", + "pattern.source_for_bg.none": "-- 选择源 --", + "pattern.delete_selected": "删除选中", + "pattern.name.hint": "此矩形布局的描述性名称", + "pattern.description.hint": "关于此图案使用位置或方式的可选说明", + "pattern.visual_editor.hint": "点击 + 按钮添加矩形。拖动边缘调整大小,拖动内部移动位置。", + "pattern.rectangles.hint": "用精确坐标(0.0 到 1.0)微调矩形位置和大小", + "overlay.toggle": "切换屏幕叠加层", + "overlay.button.show": "显示叠加层可视化", + "overlay.button.hide": "隐藏叠加层可视化", + "overlay.started": "叠加层可视化已启动", + "overlay.stopped": "叠加层可视化已停止", + "overlay.error.start": "启动叠加层失败", + "overlay.error.stop": "停止叠加层失败", + "dashboard.title": "仪表盘", + "dashboard.section.targets": "目标", + "dashboard.section.running": "运行中", + "dashboard.section.stopped": "已停止", + "dashboard.no_targets": "尚未配置目标", + "dashboard.uptime": "运行时长", + "dashboard.fps": "FPS", + "dashboard.errors": "错误", + "dashboard.device": "设备", + "dashboard.stop_all": "全部停止", + "dashboard.failed": "加载仪表盘失败", + "dashboard.section.automations": "自动化", + "dashboard.section.scenes": "场景预设", + "dashboard.section.sync_clocks": "同步时钟", + "dashboard.targets": "目标", + "dashboard.section.performance": "系统性能", + "dashboard.perf.cpu": "CPU", + "dashboard.perf.ram": "内存", + "dashboard.perf.gpu": "GPU", + "dashboard.perf.unavailable": "不可用", + "dashboard.perf.color": "图表颜色", + "dashboard.perf.mode.system": "系统", + "dashboard.perf.mode.app": "应用", + "dashboard.perf.mode.both": "全部", + "dashboard.poll_interval": "刷新间隔", + "automations.title": "自动化", + "automations.empty": "尚未配置自动化。创建一个以自动激活场景。", + "automations.add": "添加自动化", + "automations.edit": "编辑自动化", + "automations.delete.confirm": "删除自动化 \"{name}\"?", + "automations.name": "名称:", + "automations.name.hint": "此自动化的描述性名称", + "automations.name.placeholder": "我的自动化", + "automations.enabled": "启用:", + "automations.enabled.hint": "禁用的自动化即使满足条件也不会激活", + "automations.condition_logic": "条件逻辑:", + "automations.condition_logic.hint": "多个条件的组合方式:任一(或)或 全部(与)", + "automations.condition_logic.or": "任一条件(或)", + "automations.condition_logic.and": "全部条件(与)", + "automations.condition_logic.or.desc": "任一条件匹配时触发", + "automations.condition_logic.and.desc": "全部匹配时才触发", + "automations.conditions": "条件:", + "automations.conditions.hint": "决定此自动化何时激活的规则", + "automations.conditions.add": "添加条件", + "automations.conditions.empty": "无条件 — 启用后自动化始终处于活动状态", + "automations.condition.always": "始终", + "automations.condition.always.desc": "始终活跃", + "automations.condition.always.hint": "自动化启用后立即激活并保持活动。", + "automations.condition.startup": "启动", + "automations.condition.startup.desc": "服务器启动时", + "automations.condition.startup.hint": "服务器启动时激活,启用期间保持活动。", + "automations.condition.application": "应用程序", + "automations.condition.application.desc": "应用运行/聚焦", + "automations.condition.application.apps": "应用程序:", + "automations.condition.application.apps.hint": "进程名,每行一个(例如 firefox.exe)", + "automations.condition.application.browse": "浏览", + "automations.condition.application.search": "筛选进程...", + "automations.condition.application.no_processes": "未找到进程", + "automations.condition.application.match_type": "匹配类型:", + "automations.condition.application.match_type.hint": "如何检测应用程序", + "automations.condition.application.match_type.running": "运行中", + "automations.condition.application.match_type.running.desc": "进程活跃", + "automations.condition.application.match_type.topmost": "最前", + "automations.condition.application.match_type.topmost.desc": "前台窗口", + "automations.condition.application.match_type.topmost_fullscreen": "最前 + 全屏", + "automations.condition.application.match_type.topmost_fullscreen.desc": "前台 + 全屏", + "automations.condition.application.match_type.fullscreen": "全屏", + "automations.condition.application.match_type.fullscreen.desc": "任意全屏应用", + "automations.condition.time_of_day": "时段", + "automations.condition.time_of_day.desc": "时间范围", + "automations.condition.time_of_day.start_time": "开始时间:", + "automations.condition.time_of_day.end_time": "结束时间:", + "automations.condition.time_of_day.overnight_hint": "跨夜时段(如 22:00–06:00),请将开始时间设为晚于结束时间。", + "automations.condition.system_idle": "系统空闲", + "automations.condition.system_idle.desc": "空闲/活跃", + "automations.condition.system_idle.idle_minutes": "空闲超时(分钟):", + "automations.condition.system_idle.mode": "触发模式:", + "automations.condition.system_idle.when_idle": "空闲时", + "automations.condition.system_idle.when_active": "活跃时", + "automations.condition.display_state": "显示器状态", + "automations.condition.display_state.desc": "显示器开/关", + "automations.condition.display_state.state": "显示器状态:", + "automations.condition.display_state.on": "开启", + "automations.condition.display_state.off": "关闭(休眠)", + "automations.condition.mqtt": "MQTT", + "automations.condition.mqtt.desc": "MQTT 消息", + "automations.condition.mqtt.topic": "主题:", + "automations.condition.mqtt.payload": "消息内容:", + "automations.condition.mqtt.match_mode": "匹配模式:", + "automations.condition.mqtt.match_mode.exact": "精确匹配", + "automations.condition.mqtt.match_mode.contains": "包含", + "automations.condition.mqtt.match_mode.regex": "正则表达式", + "automations.condition.mqtt.hint": "当 MQTT 主题收到匹配的消息时激活", + "automations.condition.webhook": "Webhook", + "automations.condition.webhook.desc": "HTTP 回调", + "automations.condition.webhook.hint": "通过外部服务的 HTTP 请求激活(Home Assistant、IFTTT、curl 等)", + "automations.condition.webhook.url": "Webhook URL:", + "automations.condition.webhook.copy": "复制", + "automations.condition.webhook.copied": "已复制!", + "automations.condition.webhook.save_first": "请先保存自动化以生成 Webhook URL", + "automations.scene": "场景:", + "automations.scene.hint": "条件满足时激活的场景预设", + "automations.scene.search_placeholder": "搜索场景...", + "automations.scene.none_selected": "无(无场景)", + "automations.scene.none_available": "没有可用的场景", + "automations.deactivation_mode": "停用方式:", + "automations.deactivation_mode.hint": "条件不再满足时的行为", + "automations.deactivation_mode.none": "无", + "automations.deactivation_mode.none.desc": "保持当前状态", + "automations.deactivation_mode.revert": "恢复", + "automations.deactivation_mode.revert.desc": "恢复到之前的状态", + "automations.deactivation_mode.fallback_scene": "备用", + "automations.deactivation_mode.fallback_scene.desc": "激活备用场景", + "automations.deactivation_scene": "备用场景:", + "automations.deactivation_scene.hint": "自动化停用时激活的场景", + "automations.status.active": "活动", + "automations.status.inactive": "非活动", + "automations.status.disabled": "已禁用", + "automations.action.disable": "禁用", + "automations.last_activated": "上次激活", + "automations.logic.and": " 与 ", + "automations.logic.or": " 或 ", + "automations.logic.all": "全部", + "automations.logic.any": "任一", + "automations.updated": "自动化已更新", + "automations.created": "自动化已创建", + "automations.deleted": "自动化已删除", + "automations.error.name_required": "名称为必填项", + "automations.error.clone_failed": "克隆自动化失败", + "scenes.title": "场景", + "scenes.add": "捕获场景", + "scenes.edit": "编辑场景", + "scenes.name": "名称:", + "scenes.name.hint": "此场景预设的描述性名称", + "scenes.name.placeholder": "我的场景", + "scenes.description": "描述:", + "scenes.description.hint": "此场景功能的可选描述", + "scenes.targets": "目标:", + "scenes.targets.hint": "选择要包含在此场景快照中的目标", + "scenes.targets.add": "添加目标", + "scenes.targets.search_placeholder": "搜索目标...", + "scenes.capture": "捕获", + "scenes.activate": "激活场景", + "scenes.recapture": "重新捕获当前状态", + "scenes.delete": "删除场景", + "scenes.targets_count": "目标", + "scenes.captured": "场景已捕获", + "scenes.updated": "场景已更新", + "scenes.activated": "场景已激活", + "scenes.activated_partial": "场景部分激活", + "scenes.errors": "错误", + "scenes.recaptured": "场景已重新捕获", + "scenes.deleted": "场景已删除", + "scenes.recapture_confirm": "将当前状态重新捕获到\"{name}\"中?", + "scenes.delete_confirm": "删除场景\"{name}\"?", + "scenes.error.name_required": "名称为必填项", + "scenes.error.save_failed": "保存场景失败", + "scenes.error.activate_failed": "激活场景失败", + "scenes.error.recapture_failed": "重新捕获场景失败", + "scenes.error.delete_failed": "删除场景失败", + "scenes.cloned": "场景已克隆", + "scenes.error.clone_failed": "克隆场景失败", + "time.hours_minutes": "{h}时 {m}分", + "time.minutes_seconds": "{m}分 {s}秒", + "time.seconds": "{s}秒", + "dashboard.type.led": "LED", + "dashboard.type.kc": "关键颜色", + "aria.close": "关闭", + "aria.save": "保存", + "aria.cancel": "取消", + "aria.previous": "上一个", + "aria.next": "下一个", + "aria.hint": "显示提示", + "color_strip.select_type": "选择色带类型", + "color_strip.add": "添加", + "color_strip.edit": "编辑", + "color_strip.name": "名称:", + "color_strip.name.placeholder": "墙壁灯带", + "color_strip.picture_source": "图片源:", + "color_strip.picture_source.hint": "用作 LED 颜色计算输入的屏幕采集源", + "color_strip.fps": "目标 FPS:", + "color_strip.fps.hint": "LED 颜色更新的目标帧率(10-90)", + "color_strip.interpolation": "颜色模式:", + "color_strip.interpolation.hint": "如何从采样的边框像素计算 LED 颜色", + "color_strip.interpolation.average": "平均", + "color_strip.interpolation.median": "中位数", + "color_strip.interpolation.dominant": "主色", + "color_strip.interpolation.average.desc": "将所有采样像素混合为平滑颜色", + "color_strip.interpolation.median.desc": "取中间颜色值,减少异常值", + "color_strip.interpolation.dominant.desc": "使用样本中出现最频繁的颜色", + "color_strip.smoothing": "平滑:", + "color_strip.smoothing.hint": "帧间时间混合(0=无,1=完全)。减少闪烁。", + "color_strip.frame_interpolation": "帧插值:", + "color_strip.frame_interpolation.hint": "在连续采集帧之间混合,以在采集速率较低时仍以完整目标 FPS 输出。减少慢速环境过渡时的可见阶梯效应。", + "color_strip.color_corrections": "色彩校正", + "color_strip.brightness": "亮度:", + "color_strip.brightness.hint": "输出亮度倍数(0=关闭,1=不变,2=加倍)。在颜色提取后应用。", + "color_strip.saturation": "饱和度:", + "color_strip.saturation.hint": "颜色饱和度(0=灰度,1=不变,2=双倍饱和度)", + "color_strip.gamma": "伽马:", + "color_strip.gamma.hint": "伽马校正(1=无,<1=更亮的中间调,>1=更暗的中间调)", + "color_strip.test_device": "测试设备:", + "color_strip.test_device.hint": "选择一个设备,在点击边缘切换时发送测试像素", + "color_strip.leds": "LED 数量", + "color_strip.led_count": "LED 数量:", + "color_strip.led_count.hint": "物理灯带上的 LED 总数。屏幕源:0 = 从校准自动获取(未映射到边缘的额外 LED 将为黑色)。静态颜色:设置为与设备 LED 数量匹配。", + "color_strip.created": "色带源已创建", + "color_strip.updated": "色带源已更新", + "color_strip.deleted": "色带源已删除", + "color_strip.delete.confirm": "确定要删除此色带源吗?", + "color_strip.delete.referenced": "无法删除:此源正在被目标使用", + "color_strip.error.name_required": "请输入名称", + "color_strip.type": "类型:", + "color_strip.type.hint": "图片源从屏幕采集推导 LED 颜色。静态颜色用单一颜色填充所有 LED。渐变在所有 LED 上分布颜色渐变。颜色循环平滑循环用户定义的颜色列表。组合将多个源作为混合图层叠加。音频响应从实时音频输入驱动 LED。API 输入通过 REST 或 WebSocket 从外部客户端接收原始 LED 颜色。", + "color_strip.type.picture": "图片源", + "color_strip.type.picture.desc": "从屏幕捕获获取颜色", + "color_strip.type.picture_advanced": "多显示器", + "color_strip.type.picture_advanced.desc": "跨显示器的线条校准", + "color_strip.type.static": "静态颜色", + "color_strip.type.static.desc": "单色填充", + "color_strip.type.gradient": "渐变", + "color_strip.type.gradient.desc": "LED上的平滑颜色过渡", + "color_strip.type.color_cycle": "颜色循环", + "color_strip.type.color_cycle.desc": "循环切换颜色列表", + "color_strip.static_color": "颜色:", + "color_strip.static_color.hint": "将发送到灯带上所有 LED 的纯色。", + "color_strip.gradient.preview": "渐变:", + "color_strip.gradient.preview.hint": "可视预览。点击下方标记轨道添加色标。拖动标记重新定位。", + "color_strip.gradient.stops": "色标:", + "color_strip.gradient.stops.hint": "每个色标在相对位置定义一种颜色(0.0 = 起始,1.0 = 结束)。↔ 按钮添加右侧颜色以在该色标处创建硬边。", + "color_strip.gradient.stops_count": "个色标", + "color_strip.gradient.add_stop": "+ 添加色标", + "color_strip.gradient.position": "位置(0.0-1.0)", + "color_strip.gradient.bidir.hint": "在此色标右侧添加第二种颜色以在渐变中创建硬边。", + "color_strip.gradient.min_stops": "渐变至少需要 2 个色标", + "color_strip.gradient.preset": "预设:", + "color_strip.gradient.preset.hint": "加载预定义的渐变调色板。选择预设将替换当前色标。", + "color_strip.gradient.preset.custom": "— 自定义 —", + "color_strip.gradient.preset.rainbow": "彩虹", + "color_strip.gradient.preset.sunset": "日落", + "color_strip.gradient.preset.ocean": "海洋", + "color_strip.gradient.preset.forest": "森林", + "color_strip.gradient.preset.fire": "火焰", + "color_strip.gradient.preset.lava": "熔岩", + "color_strip.gradient.preset.aurora": "极光", + "color_strip.gradient.preset.ice": "冰", + "color_strip.gradient.preset.warm": "暖色", + "color_strip.gradient.preset.cool": "冷色", + "color_strip.gradient.preset.neon": "霓虹", + "color_strip.gradient.preset.pastel": "柔和", + "color_strip.gradient.preset.save_button": "保存为预设…", + "color_strip.gradient.preset.save_prompt": "输入预设名称:", + "color_strip.gradient.preset.saved": "预设已保存", + "color_strip.gradient.preset.deleted": "预设已删除", + "color_strip.gradient.preset.apply": "应用", + "color_strip.animation": "动画", + "color_strip.animation.type": "效果:", + "color_strip.animation.type.hint": "要应用的动画效果。", + "color_strip.animation.type.none": "无(无动画效果)", + "color_strip.animation.type.none.desc": "静态颜色,无动画", + "color_strip.animation.type.breathing": "呼吸", + "color_strip.animation.type.breathing.desc": "平滑的亮度渐入渐出", + "color_strip.animation.type.color_cycle": "颜色循环", + "color_strip.animation.type.gradient_shift": "渐变移动", + "color_strip.animation.type.gradient_shift.desc": "渐变沿灯带滑动", + "color_strip.animation.type.wave": "波浪", + "color_strip.animation.type.wave.desc": "沿灯带移动的正弦亮度波", + "color_strip.animation.type.strobe": "频闪", + "color_strip.animation.type.strobe.desc": "快速开/关闪烁", + "color_strip.animation.type.sparkle": "闪烁", + "color_strip.animation.type.sparkle.desc": "随机 LED 短暂闪亮", + "color_strip.animation.type.pulse": "脉冲", + "color_strip.animation.type.pulse.desc": "快速衰减的尖锐亮度脉冲", + "color_strip.animation.type.candle": "烛光", + "color_strip.animation.type.candle.desc": "温暖的类似蜡烛的闪烁光芒", + "color_strip.animation.type.rainbow_fade": "彩虹渐变", + "color_strip.animation.type.rainbow_fade.desc": "循环整个色相光谱", + "color_strip.animation.speed": "速度:", + "color_strip.animation.speed.hint": "动画速度倍数。1.0 ≈ 呼吸效果每秒一个循环;更高值循环更快。", + "color_strip.color_cycle.colors": "颜色:", + "color_strip.color_cycle.colors.hint": "平滑循环的颜色列表。至少需要 2 种。默认为全彩虹光谱。", + "color_strip.color_cycle.add_color": "+ 添加颜色", + "color_strip.color_cycle.speed": "速度:", + "color_strip.color_cycle.speed.hint": "循环速度倍数。1.0 ≈ 每 20 秒一个完整循环;更高值循环更快。", + "color_strip.color_cycle.min_colors": "颜色循环至少需要 2 种颜色", + "color_strip.type.effect": "效果", + "color_strip.type.effect.desc": "程序化效果:火焰、等离子、极光", + "color_strip.type.effect.hint": "实时生成的程序化 LED 效果(火焰、流星、等离子、噪声、极光)。", + "color_strip.type.composite": "组合", + "color_strip.type.composite.desc": "叠加和混合多个源", + "color_strip.type.composite.hint": "将多个色带源作为图层叠加,支持混合模式和不透明度。", + "color_strip.type.mapped": "映射", + "color_strip.type.mapped.desc": "为LED区域分配源", + "color_strip.type.mapped.hint": "将不同色带源分配到不同 LED 范围(区域)。与组合的图层混合不同,映射将源并排放置。", + "color_strip.type.audio": "音频响应", + "color_strip.type.audio.desc": "由音频输入驱动LED", + "color_strip.type.audio.hint": "LED 颜色由实时音频输入驱动 — 系统音频或麦克风。", + "color_strip.type.api_input": "API 输入", + "color_strip.type.api_input.desc": "从外部应用接收颜色", + "color_strip.type.api_input.hint": "通过 REST POST 或 WebSocket 从外部客户端接收原始 LED 颜色数组。用于与自定义软件、家庭自动化或任何能发送 HTTP 请求的系统集成。", + "color_strip.api_input.fallback_color": "备用颜色:", + "color_strip.api_input.fallback_color.hint": "超时未收到数据时显示的颜色。启动时和连接丢失后 LED 将显示此颜色。", + "color_strip.api_input.timeout": "超时(秒):", + "color_strip.api_input.timeout.hint": "等待新颜色数据多长时间后恢复为备用颜色。设为 0 表示永不超时。", + "color_strip.api_input.endpoints": "推送端点:", + "color_strip.api_input.endpoints.hint": "使用这些 URL 从外部应用程序推送 LED 颜色数据。REST 接受 JSON,WebSocket 接受 JSON 和原始二进制帧。", + "color_strip.api_input.save_first": "请先保存源以查看推送端点 URL。", + "color_strip.api_input.interpolation": "LED 插值:", + "color_strip.api_input.interpolation.hint": "当传入的 LED 数量与设备 LED 数量不同时如何调整大小。线性提供平滑混合,最近邻保持锐利边缘,无则截断或补零。", + "color_strip.api_input.interpolation.linear": "线性", + "color_strip.api_input.interpolation.linear.desc": "LED 之间平滑混合", + "color_strip.api_input.interpolation.nearest": "最近邻", + "color_strip.api_input.interpolation.nearest.desc": "锐利边缘,无混合", + "color_strip.api_input.interpolation.none": "无", + "color_strip.api_input.interpolation.none.desc": "截断或补零", + "color_strip.type.notification": "通知", + "color_strip.type.notification.desc": "通过Webhook触发的一次性效果", + "color_strip.type.notification.hint": "通过 Webhook 触发时显示一次性视觉效果(闪烁、脉冲、扫描)。设计为组合源中的叠加层。", + "color_strip.notification.os_listener": "监听系统通知:", + "color_strip.notification.os_listener.hint": "启用后,当桌面通知出现时(Windows toast / Linux D-Bus),此源会自动触发。需要应用具有通知访问权限。", + "color_strip.notification.effect": "效果:", + "color_strip.notification.effect.hint": "通知触发时的视觉效果。闪烁线性衰减,脉冲平滑钟形曲线,扫描从左到右填充后衰减。", + "color_strip.notification.effect.flash": "闪烁", + "color_strip.notification.effect.flash.desc": "瞬时点亮,线性衰减", + "color_strip.notification.effect.pulse": "脉冲", + "color_strip.notification.effect.pulse.desc": "平滑钟形发光", + "color_strip.notification.effect.sweep": "扫描", + "color_strip.notification.effect.sweep.desc": "从左到右填充然后消失", + "color_strip.notification.duration": "持续时间(毫秒):", + "color_strip.notification.duration.hint": "通知效果播放的时长(毫秒)。", + "color_strip.notification.default_color": "默认颜色:", + "color_strip.notification.default_color.hint": "当通知没有应用特定颜色映射时使用的颜色。", + "color_strip.notification.filter_mode": "应用过滤:", + "color_strip.notification.filter_mode.hint": "按应用名称过滤通知。关闭=接受全部,白名单=仅列出的应用,黑名单=排除列出的应用。", + "color_strip.notification.filter_mode.off": "关闭", + "color_strip.notification.filter_mode.whitelist": "白名单", + "color_strip.notification.filter_mode.blacklist": "黑名单", + "color_strip.notification.filter_mode.off.desc": "接受所有通知", + "color_strip.notification.filter_mode.whitelist.desc": "仅列出的应用", + "color_strip.notification.filter_mode.blacklist.desc": "排除列出的应用", + "color_strip.notification.filter_list": "应用列表:", + "color_strip.notification.filter_list.hint": "每行一个应用名称。使用「浏览」从运行中的进程中选择。", + "color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram", + "color_strip.notification.app_colors": "应用颜色", + "color_strip.notification.app_colors.label": "颜色映射:", + "color_strip.notification.app_colors.hint": "每个应用的自定义通知颜色。每行将一个应用名称映射到特定颜色。", + "color_strip.notification.app_colors.add": "+ 添加映射", + "color_strip.notification.app_overrides": "按应用覆盖", + "color_strip.notification.app_overrides.label": "应用覆盖:", + "color_strip.notification.app_overrides.hint": "为特定应用自定义颜色和声音。每行可设置颜色、声音资源和音量。", + "color_strip.notification.app_overrides.add": "+ 添加覆盖", + "color_strip.notification.app_overrides.app_placeholder": "应用名称", + "color_strip.notification.sound": "声音", + "color_strip.notification.sound.asset": "声音资源:", + "color_strip.notification.sound.asset.hint": "选择通知触发时播放的声音资源。留空表示静音。", + "color_strip.notification.sound.none": "无(静音)", + "color_strip.notification.sound.search": "搜索声音…", + "color_strip.notification.sound.volume": "音量:", + "color_strip.notification.sound.volume.hint": "通知声音的全局音量(0–100%)。", + "color_strip.notification.sound.app_sounds": "按应用声音:", + "color_strip.notification.sound.app_sounds.hint": "为特定应用覆盖声音和音量。空声音 = 静音该应用。", + "color_strip.notification.sound.app_sounds.add": "+ 添加覆盖", + "color_strip.notification.sound.app_name_placeholder": "应用名称", + "color_strip.notification.endpoint": "Webhook 端点:", + "color_strip.notification.endpoint.hint": "使用此 URL 从外部系统触发通知。POST 请求可选 JSON:{\"app\": \"AppName\", \"color\": \"#FF0000\"}。", + "color_strip.notification.save_first": "请先保存源以查看 Webhook 端点 URL。", + "color_strip.notification.app_count": "个应用", + "color_strip.notification.test": "测试通知", + "color_strip.notification.test.ok": "通知已发送", + "color_strip.notification.test.no_streams": "此源没有运行中的流", + "color_strip.notification.test.error": "发送通知失败", + "color_strip.notification.history.title": "通知历史", + "color_strip.notification.history.hint": "监听器捕获的最近OS通知(最新在前),最多50条。", + "color_strip.notification.history.empty": "尚未捕获任何通知", + "color_strip.notification.history.unavailable": "此平台不支持OS通知监听器", + "color_strip.notification.history.error": "加载通知历史失败", + "color_strip.notification.history.refresh": "刷新", + "color_strip.notification.history.unknown_app": "未知应用", + "color_strip.notification.history.fired": "触发的流数量", + "color_strip.notification.history.filtered": "过滤的流数量", + "color_strip.test.title": "预览测试", + "color_strip.test.connecting": "连接中...", + "color_strip.test.error": "无法连接到预览流", + "color_strip.test.led_count": "LED数量:", + "color_strip.test.fps": "FPS:", + "color_strip.test.receive_fps": "接收帧率", + "color_strip.test.apply": "应用", + "color_strip.test.composite": "合成", + "color_strip.preview.title": "实时预览", + "color_strip.preview.not_connected": "未连接", + "color_strip.preview.connecting": "连接中...", + "color_strip.preview.connected": "已连接", + "color_strip.preview.unsupported": "此源类型不支持预览", + "color_strip.type.daylight": "日光循环", + "color_strip.type.daylight.desc": "模拟24小时自然日光变化", + "color_strip.type.daylight.hint": "模拟太阳在24小时内的色温变化——从温暖的日出到冷白的日光,再到温暖的日落和昏暗的夜晚。", + "color_strip.daylight.speed": "速度:", + "color_strip.daylight.speed.hint": "循环速度倍数。1.0 = 约4分钟完成一个完整的昼夜循环。", + "color_strip.daylight.use_real_time": "使用实时时间:", + "color_strip.daylight.use_real_time.hint": "启用后,LED颜色匹配计算机的实际时间。速度设置将被忽略。", + "color_strip.daylight.real_time": "实时", + "color_strip.daylight.latitude": "纬度:", + "color_strip.daylight.latitude.hint": "地理纬度(-90到90)。影响实时模式下的日出/日落时间。", + "color_strip.type.candlelight": "烛光", + "color_strip.type.candlelight.desc": "逼真的烛光闪烁模拟", + "color_strip.type.candlelight.hint": "在所有LED上模拟逼真的蜡烛闪烁,具有温暖色调和有机闪烁模式。", + "color_strip.type.weather": "天气", + "color_strip.type.weather.desc": "天气感应环境色彩", + "color_strip.type.weather.hint": "将实时天气状况映射为环境LED颜色。需要天气源实体。", + "color_strip.weather.source": "天气源:", + "color_strip.weather.source.hint": "用于环境颜色的天气数据源。请先在天气标签页中创建。", + "color_strip.weather.speed": "动画速度:", + "color_strip.weather.speed.hint": "环境色彩漂移动画速度。越高越快。", + "color_strip.weather.temperature_influence": "温度影响:", + "color_strip.weather.temperature_influence.hint": "当前温度对调色板冷暖偏移程度。0=纯天气颜色,1=强偏移。", + "color_strip.weather.error.no_source": "请选择天气源", + "color_strip.candlelight.color": "基础颜色:", + "color_strip.candlelight.color.hint": "蜡烛火焰的温暖基础颜色。默认为自然温暖的琥珀色。", + "color_strip.candlelight.intensity": "闪烁强度:", + "color_strip.candlelight.intensity.hint": "蜡烛闪烁程度。低值产生柔和光芒,高值模拟风中的蜡烛。", + "color_strip.candlelight.num_candles_label": "蜡烛数量:", + "color_strip.candlelight.num_candles": "支蜡烛", + "color_strip.candlelight.num_candles.hint": "灯带上独立蜡烛光源的数量。每支蜡烛有自己的闪烁模式。", + "color_strip.candlelight.speed": "闪烁速度:", + "color_strip.candlelight.speed.hint": "闪烁动画的速度。较高的值产生更快、更不安定的火焰。", + "color_strip.type.processed": "已处理", + "color_strip.type.processed.desc": "将处理模板应用于另一个源", + "color_strip.type.processed.hint": "包装现有色带源并通过滤镜链处理其输出。", + "color_strip.processed.input": "源:", + "color_strip.processed.input.hint": "将被处理的色带源", + "color_strip.processed.template": "处理模板:", + "color_strip.processed.template.hint": "应用于输入源输出的滤镜链", + "color_strip.processed.error.no_input": "请选择输入源", + "color_strip.composite.layers": "图层:", + "color_strip.composite.layers.hint": "叠加多个色带源。第一个图层在底部,最后一个在顶部。每个图层可以有自己的混合模式和不透明度。", + "color_strip.composite.add_layer": "+ 添加图层", + "color_strip.composite.source": "源", + "color_strip.composite.blend_mode": "混合", + "color_strip.composite.blend_mode.normal": "正常", + "color_strip.composite.blend_mode.normal.desc": "标准 Alpha 混合", + "color_strip.composite.blend_mode.add": "叠加", + "color_strip.composite.blend_mode.add.desc": "通过叠加颜色提亮", + "color_strip.composite.blend_mode.multiply": "正片叠底", + "color_strip.composite.blend_mode.multiply.desc": "通过相乘颜色变暗", + "color_strip.composite.blend_mode.screen": "滤色", + "color_strip.composite.blend_mode.screen.desc": "提亮,正片叠底的反转", + "color_strip.composite.blend_mode.override": "覆盖", + "color_strip.composite.blend_mode.overlay": "叠加", + "color_strip.composite.blend_mode.overlay.desc": "暗部相乘,亮部滤色", + "color_strip.composite.blend_mode.soft_light": "柔光", + "color_strip.composite.blend_mode.soft_light.desc": "柔和对比度调整", + "color_strip.composite.blend_mode.hard_light": "强光", + "color_strip.composite.blend_mode.hard_light.desc": "强对比度,鲜艳色彩", + "color_strip.composite.blend_mode.difference": "差值", + "color_strip.composite.blend_mode.difference.desc": "绝对颜色差异", + "color_strip.composite.blend_mode.exclusion": "排除", + "color_strip.composite.blend_mode.exclusion.desc": "类似差值,对比度更低", + "color_strip.composite.blend_mode.override.desc": "黑色=透明,亮色=不透明", + "color_strip.composite.opacity": "不透明度", + "color_strip.composite.brightness": "亮度", + "color_strip.composite.brightness.none": "无(全亮度)", + "color_strip.composite.processing": "处理", + "color_strip.composite.enabled": "启用", + "color_strip.composite.error.min_layers": "至少需要 1 个图层", + "color_strip.composite.error.no_source": "每个图层必须选择一个源", + "color_strip.composite.layers_count": "个图层", + "color_strip.composite.range": "LED范围", + "color_strip.composite.range_start": "起始", + "color_strip.composite.range_end": "结束", + "color_strip.composite.reverse": "反转", + "color_strip.mapped.zones": "区域:", + "color_strip.mapped.zones.hint": "每个区域将色带源映射到特定 LED 范围。区域并排放置 — 区域之间的间隙保持黑色。", + "color_strip.mapped.add_zone": "+ 添加区域", + "color_strip.mapped.zone_source": "源", + "color_strip.mapped.zone_start": "起始 LED", + "color_strip.mapped.zone_end": "结束 LED", + "color_strip.mapped.zone_reverse": "反转", + "color_strip.mapped.zones_count": "个区域", + "color_strip.mapped.select_source": "搜索源...", + "color_strip.mapped.error.no_source": "每个区域必须选择一个源", + "color_strip.audio.visualization": "可视化:", + "color_strip.audio.visualization.hint": "音频数据如何渲染到 LED。", + "color_strip.audio.viz.spectrum": "频谱分析", + "color_strip.audio.viz.spectrum.desc": "频率条分布在灯带上", + "color_strip.audio.viz.beat_pulse": "节拍脉冲", + "color_strip.audio.viz.beat_pulse.desc": "所有LED随节拍脉动", + "color_strip.audio.viz.vu_meter": "VU 表", + "color_strip.audio.viz.vu_meter.desc": "音量填充灯带", + "color_strip.audio.source": "音频源:", + "color_strip.audio.source.hint": "此可视化的音频源。可以是多声道(设备)或单声道(单通道)源。在源标签页中创建和管理音频源。", + "color_strip.audio.sensitivity": "灵敏度:", + "color_strip.audio.sensitivity.hint": "音频电平的增益倍数。更高值使 LED 对较安静的声音也有反应。", + "color_strip.audio.smoothing": "平滑:", + "color_strip.audio.smoothing.hint": "帧间时间平滑。更高值产生更平滑但反应较慢的视觉效果。", + "color_strip.audio.palette": "调色板:", + "color_strip.audio.palette.hint": "用于频谱条或节拍脉冲着色的调色板。", + "color_strip.audio.color": "基础颜色:", + "color_strip.audio.color.hint": "VU 表条的低电平颜色。", + "color_strip.audio.color_peak": "峰值颜色:", + "color_strip.audio.color_peak.hint": "VU 表条顶部的高电平颜色。", + "color_strip.audio.mirror": "镜像:", + "color_strip.audio.mirror.hint": "从中心向外镜像频谱:低音在中间,高音在两端。", + "color_strip.effect.type": "效果类型:", + "color_strip.effect.type.hint": "选择程序化算法。", + "color_strip.effect.fire": "火焰", + "color_strip.effect.fire.desc": "模拟带热量扩散的上升火焰的元胞自动机", + "color_strip.effect.meteor": "流星", + "color_strip.effect.meteor.desc": "明亮头部沿灯带移动,带指数衰减的尾迹", + "color_strip.effect.plasma": "等离子", + "color_strip.effect.plasma.desc": "映射到调色板的重叠正弦波 — 经典演示场景效果", + "color_strip.effect.noise": "噪声", + "color_strip.effect.noise.desc": "滚动的分形值噪声映射到调色板", + "color_strip.effect.aurora": "极光", + "color_strip.effect.aurora.desc": "漂移和混合的分层噪声带 — 北极光风格", + "color_strip.effect.speed": "速度:", + "color_strip.effect.speed.hint": "效果动画的速度倍数(0.1 = 非常慢,10.0 = 非常快)。", + "color_strip.effect.palette": "调色板:", + "color_strip.effect.palette.hint": "用于将效果值映射到 RGB 颜色的调色板。", + "color_strip.effect.color": "流星颜色:", + "color_strip.effect.color.hint": "流星效果的头部颜色。", + "color_strip.effect.intensity": "强度:", + "color_strip.effect.intensity.hint": "效果强度 — 控制火花率(火焰)、尾迹衰减(流星)或亮度范围(极光)。", + "color_strip.effect.scale": "比例:", + "color_strip.effect.scale.hint": "空间比例 — 波频率(等离子)、缩放级别(噪声)或带宽(极光)。", + "color_strip.effect.mirror": "镜像:", + "color_strip.effect.mirror.hint": "反弹模式 — 流星在灯带末端反转方向而不是循环。", + "color_strip.palette.fire": "火焰", + "color_strip.palette.ocean": "海洋", + "color_strip.palette.lava": "熔岩", + "color_strip.palette.forest": "森林", + "color_strip.palette.rainbow": "彩虹", + "color_strip.palette.aurora": "极光", + "color_strip.palette.sunset": "日落", + "color_strip.palette.ice": "冰", + "audio_source.title": "音频源", + "audio_source.group.multichannel": "多声道", + "audio_source.group.mono": "单声道", + "audio_source.group.band_extract": "频段提取", + "audio_source.add": "添加音频源", + "audio_source.add.multichannel": "添加多声道源", + "audio_source.add.mono": "添加单声道源", + "audio_source.add.band_extract": "添加频段提取源", + "audio_source.edit": "编辑音频源", + "audio_source.edit.multichannel": "编辑多声道源", + "audio_source.edit.mono": "编辑单声道源", + "audio_source.edit.band_extract": "编辑频段提取源", + "audio_source.name": "名称:", + "audio_source.name.placeholder": "系统音频", + "audio_source.name.hint": "此音频源的描述性名称", + "audio_source.type": "类型:", + "audio_source.type.hint": "多声道从物理音频设备采集所有通道。单声道从多声道源提取单个通道。", + "audio_source.type.multichannel": "多声道", + "audio_source.type.mono": "单声道", + "audio_source.device": "音频设备:", + "audio_source.device.hint": "音频输入源。回环设备采集系统音频输出;输入设备采集麦克风或线路输入。", + "audio_source.refresh_devices": "刷新设备", + "audio_source.parent": "父源:", + "audio_source.parent.hint": "要从中提取通道的多声道源", + "audio_source.channel": "通道:", + "audio_source.channel.hint": "从多声道源提取哪个音频通道", + "audio_source.channel.mono": "单声道(左+右混合)", + "audio_source.channel.left": "左", + "audio_source.channel.right": "右", + "audio_source.description": "描述(可选):", + "audio_source.description.placeholder": "描述此音频源...", + "audio_source.description.hint": "关于此音频源的可选说明", + "audio_source.created": "音频源已创建", + "audio_source.updated": "音频源已更新", + "audio_source.deleted": "音频源已删除", + "audio_source.delete.confirm": "确定要删除此音频源吗?", + "audio_source.error.name_required": "请输入名称", + "audio_source.audio_template": "音频模板:", + "audio_source.audio_template.hint": "定义此设备使用哪个引擎和设置的音频采集模板", + "audio_source.band_parent": "父音频源:", + "audio_source.band_parent.hint": "要从中提取频段的音频源", + "audio_source.band": "频段:", + "audio_source.band.hint": "选择频段预设或自定义范围", + "audio_source.band.bass": "低音 (20–250 Hz)", + "audio_source.band.mid": "中音 (250–4000 Hz)", + "audio_source.band.treble": "高音 (4000–20000 Hz)", + "audio_source.band.custom": "自定义范围", + "audio_source.freq_low": "低频 (Hz):", + "audio_source.freq_high": "高频 (Hz):", + "audio_source.freq_range": "频率范围", + "audio_source.test": "测试", + "audio_source.test.title": "测试音频源", + "audio_source.test.rms": "RMS", + "audio_source.test.peak": "峰值", + "audio_source.test.beat": "节拍", + "audio_source.test.connecting": "连接中...", + "audio_source.test.error": "音频测试失败", + "audio_template.test": "测试", + "audio_template.test.title": "测试音频模板", + "audio_template.test.device": "音频设备:", + "audio_template.test.device.hint": "选择测试期间要采集的音频设备", + "audio_template.test.run": "运行", + "audio_template.title": "音频模板", + "audio_template.add": "添加音频模板", + "audio_template.edit": "编辑音频模板", + "audio_template.name": "模板名称:", + "audio_template.name.placeholder": "我的音频模板", + "audio_template.description.label": "描述(可选):", + "audio_template.description.placeholder": "描述此模板...", + "audio_template.engine": "音频引擎:", + "audio_template.engine.hint": "选择要使用的音频采集后端。WASAPI 仅限 Windows 并支持回环。Sounddevice 支持跨平台。", + "audio_template.engine.unavailable": "不可用", + "audio_template.engine.unavailable.hint": "此引擎在您的系统上不可用", + "audio_template.config": "配置", + "audio_template.config.show": "显示配置", + "audio_template.created": "音频模板已创建", + "audio_template.updated": "音频模板已更新", + "audio_template.deleted": "音频模板已删除", + "audio_template.delete.confirm": "确定要删除此音频模板吗?", + "audio_template.error.load": "加载音频模板失败", + "audio_template.error.engines": "加载音频引擎失败", + "audio_template.error.required": "请填写所有必填项", + "audio_template.error.delete": "删除音频模板失败", + "streams.group.value": "值源", + "streams.group.sync": "同步时钟", + "tree.group.picture": "图片源", + "tree.group.capture": "屏幕采集", + "tree.group.static": "静态", + "tree.group.processing": "已处理", + "tree.group.strip": "色带", + "tree.group.audio": "音频", + "tree.group.utility": "工具", + "tree.leaf.sources": "源", + "tree.leaf.engine_templates": "引擎模板", + "tree.leaf.images": "图片", + "tree.leaf.video": "视频", + "tree.leaf.filter_templates": "滤镜模板", + "tree.leaf.processing_templates": "处理模板", + "tree.leaf.templates": "模板", + "value_source.group.title": "值源", + "value_source.select_type": "选择值源类型", + "value_source.add": "添加值源", + "value_source.edit": "编辑值源", + "value_source.name": "名称:", + "value_source.name.placeholder": "亮度脉冲", + "value_source.name.hint": "此值源的描述性名称", + "value_source.type": "类型:", + "value_source.type.hint": "静态输出固定值。动画循环波形。音频响应声音输入。自适应类型根据时间或场景内容自动调节亮度。", + "value_source.type.static": "静态", + "value_source.type.static.desc": "固定输出值", + "value_source.type.animated": "动画", + "value_source.type.animated.desc": "循环波形变化", + "value_source.type.audio": "音频", + "value_source.type.audio.desc": "响应声音输入", + "value_source.type.adaptive_time": "自适应(时间)", + "value_source.type.adaptive_time.desc": "按时间自动调节", + "value_source.type.adaptive_scene": "自适应(场景)", + "value_source.type.adaptive_scene.desc": "按场景内容调节", + "value_source.type.daylight": "日光周期", + "value_source.type.daylight.desc": "亮度跟随日夜周期", + "value_source.daylight.speed": "速度:", + "value_source.daylight.speed.hint": "周期速度倍率。1.0 = 完整日夜周期约4分钟。", + "value_source.daylight.use_real_time": "使用实时:", + "value_source.daylight.use_real_time.hint": "启用后,亮度跟随实际时间。速度设置将被忽略。", + "value_source.daylight.enable_real_time": "跟随系统时钟", + "value_source.daylight.latitude": "纬度:", + "value_source.daylight.latitude.hint": "地理纬度(-90到90)。影响实时模式下的日出/日落时间。", + "value_source.daylight.real_time": "实时", + "value_source.daylight.speed_label": "速度", + "value_source.value": "值:", + "value_source.value.hint": "固定输出值(0.0 = 关闭,1.0 = 最大亮度)", + "value_source.waveform": "波形:", + "value_source.waveform.hint": "亮度动画循环的形状", + "value_source.waveform.sine": "正弦", + "value_source.waveform.triangle": "三角", + "value_source.waveform.square": "方波", + "value_source.waveform.sawtooth": "锯齿", + "value_source.speed": "速度(周期/分):", + "value_source.speed.hint": "每分钟周期数 — 波形重复的速度(1 = 非常慢,120 = 非常快)", + "value_source.min_value": "最小值:", + "value_source.min_value.hint": "波形周期的最小输出", + "value_source.max_value": "最大值:", + "value_source.max_value.hint": "波形周期的最大输出", + "value_source.audio_source": "音频源:", + "value_source.audio_source.hint": "要读取音频电平的音频源(多声道或单声道)", + "value_source.mode": "模式:", + "value_source.mode.hint": "RMS 测量平均音量。峰值跟踪最响的时刻。节拍在节奏上触发。", + "value_source.mode.rms": "RMS(音量)", + "value_source.mode.peak": "峰值", + "value_source.mode.beat": "节拍", + "value_source.mode.rms.desc": "平均音量水平", + "value_source.mode.peak.desc": "最响时刻追踪", + "value_source.mode.beat.desc": "节奏脉冲检测", + "value_source.auto_gain": "自动增益:", + "value_source.auto_gain.hint": "自动归一化音频电平,使输出使用完整范围,无论输入音量大小", + "value_source.auto_gain.enable": "启用自动增益", + "value_source.sensitivity": "灵敏度:", + "value_source.sensitivity.hint": "音频信号的增益倍数(越高反应越灵敏)", + "value_source.scene_sensitivity.hint": "亮度信号的增益倍数(越高对亮度变化越敏感)", + "value_source.smoothing": "平滑:", + "value_source.smoothing.hint": "时间平滑(0 = 即时响应,1 = 非常平滑/缓慢)", + "value_source.audio_min_value": "最小值:", + "value_source.audio_min_value.hint": "音频静默时的输出(例如 0.3 = 30% 亮度下限)", + "value_source.audio_max_value": "最大值:", + "value_source.audio_max_value.hint": "最大音频电平时的输出", + "value_source.schedule": "计划:", + "value_source.schedule.hint": "定义至少 2 个时间点。亮度在各点之间线性插值,午夜循环。", + "value_source.schedule.add": "+ 添加时间点", + "value_source.schedule.points": "个时间点", + "value_source.picture_source": "图片源:", + "value_source.picture_source.hint": "将分析其帧以获取平均亮度的图片源。", + "value_source.scene_behavior": "行为:", + "value_source.scene_behavior.hint": "互补:暗场景 = 高亮度(适合环境背光)。匹配:亮场景 = 高亮度。", + "value_source.scene_behavior.complement": "互补(暗 → 亮)", + "value_source.scene_behavior.match": "匹配(亮 → 亮)", + "value_source.adaptive_min_value": "最小值:", + "value_source.adaptive_min_value.hint": "最小输出亮度", + "value_source.adaptive_max_value": "最大值:", + "value_source.adaptive_max_value.hint": "最大输出亮度", + "value_source.error.schedule_min": "计划至少需要 2 个时间点", + "value_source.description": "描述(可选):", + "value_source.description.placeholder": "描述此值源...", + "value_source.description.hint": "关于此值源的可选说明", + "value_source.created": "值源已创建", + "value_source.updated": "值源已更新", + "value_source.deleted": "值源已删除", + "value_source.delete.confirm": "确定要删除此值源吗?", + "value_source.error.name_required": "请输入名称", + "value_source.test": "测试", + "value_source.test.title": "测试值源", + "value_source.test.connecting": "连接中...", + "value_source.test.error": "连接失败", + "value_source.test.current": "当前", + "value_source.test.min": "最小", + "value_source.test.max": "最大", + "test.frames": "帧数", + "test.fps": "帧率", + "test.avg_capture": "平均", + "targets.brightness_vs": "亮度源:", + "targets.brightness_vs.hint": "可选的值源,每帧动态控制亮度(覆盖设备亮度)", + "targets.brightness_vs.none": "无(设备亮度)", + "targets.min_brightness_threshold": "最低亮度阈值:", + "targets.min_brightness_threshold.hint": "当有效输出亮度(像素亮度 × 设备/源亮度)低于此值时,LED完全关闭(0 = 禁用)", + "targets.adaptive_fps": "自适应FPS:", + "targets.adaptive_fps.hint": "当设备无响应时自动降低发送速率,稳定后逐步恢复。推荐用于信号较弱的WiFi设备。", + "targets.protocol": "协议:", + "targets.protocol.hint": "DDP通过快速UDP发送像素(推荐)。HTTP使用JSON API——较慢但可靠,限制约500个LED。", + "targets.protocol.ddp": "DDP (UDP)", + "targets.protocol.ddp.desc": "快速UDP数据包 - 推荐", + "targets.protocol.http": "HTTP", + "targets.protocol.http.desc": "JSON API - 较慢,≤500 LED", + "targets.protocol.serial": "串口", + "search.open": "搜索 (Ctrl+K)", + "search.placeholder": "搜索实体... (Ctrl+K)", + "search.loading": "加载中...", + "search.no_results": "未找到结果", + "search.group.devices": "设备", + "search.group.targets": "LED 目标", + "search.group.kc_targets": "关键颜色目标", + "search.group.css": "色带源", + "search.group.automations": "自动化", + "search.group.streams": "图片流", + "search.group.capture_templates": "采集模板", + "search.group.pp_templates": "后处理模板", + "search.group.pattern_templates": "图案模板", + "search.group.audio": "音频源", + "search.group.value": "值源", + "search.group.scenes": "场景预设", + "search.group.cspt": "色带处理模板", + "search.group.sync_clocks": "同步时钟", + "search.group.actions": "操作", + "search.action.start": "启动", + "search.action.stop": "停止", + "search.action.activate": "激活", + "search.action.enable": "启用", + "search.action.disable": "禁用", + "settings.backup.label": "备份配置", + "settings.backup.hint": "将所有配置(设备、目标、流、模板、自动化)下载为单个 JSON 文件。", + "settings.backup.button": "下载备份", + "settings.backup.success": "备份下载成功", + "settings.backup.error": "备份下载失败", + "settings.restore.label": "恢复配置", + "settings.restore.hint": "上传之前下载的备份文件以替换所有配置。服务器将自动重启。", + "settings.restore.button": "从备份恢复", + "settings.restore.confirm": "这将替换所有配置并重启服务器。确定继续吗?", + "settings.restore.success": "配置已恢复", + "settings.restore.error": "恢复失败", + "settings.restore.restarting": "服务器正在重启...", + "settings.restore.restart_timeout": "服务器未响应。请手动刷新页面。", + "settings.restart_server": "重启服务器", + "settings.restart_confirm": "重启服务器?活跃的目标将被停止。", + "settings.restarting": "正在重启服务器...", + "settings.button.close": "关闭", + "settings.log_level.label": "日志级别", + "settings.log_level.hint": "实时更改服务器日志详细程度。DEBUG 显示最多细节;CRITICAL 仅显示致命错误。", + "settings.log_level.save": "应用", + "settings.log_level.saved": "日志级别已更改", + "settings.log_level.save_error": "更改日志级别失败", + "settings.log_level.desc.debug": "详细开发输出", + "settings.log_level.desc.info": "正常运行消息", + "settings.log_level.desc.warning": "潜在问题", + "settings.log_level.desc.error": "仅显示错误", + "settings.log_level.desc.critical": "仅显示致命错误", + "settings.auto_backup.label": "自动备份", + "settings.auto_backup.hint": "自动定期创建所有配置的备份。当达到最大数量时,旧备份会被自动清理。", + "settings.auto_backup.enable": "启用自动备份", + "settings.auto_backup.interval_label": "间隔", + "settings.auto_backup.max_label": "最大备份数", + "settings.auto_backup.save": "保存设置", + "settings.auto_backup.saved": "自动备份设置已保存", + "settings.auto_backup.save_error": "保存自动备份设置失败", + "settings.auto_backup.backup_now": "立即备份", + "settings.auto_backup.backup_created": "备份已创建", + "settings.auto_backup.backup_error": "备份失败", + "settings.auto_backup.last_backup": "上次备份", + "settings.auto_backup.never": "从未", + "settings.saved_backups.label": "已保存的备份", + "settings.saved_backups.hint": "存储在服务器上的自动备份文件。下载到本地保存,或删除以释放空间。", + "settings.saved_backups.empty": "没有已保存的备份", + "settings.saved_backups.restore": "恢复", + "settings.saved_backups.download": "下载", + "settings.saved_backups.delete": "删除", + "settings.saved_backups.delete_confirm": "删除此备份文件?", + "settings.saved_backups.delete_error": "删除备份失败", + "settings.saved_backups.type.auto": "自动", + "settings.saved_backups.type.manual": "手动", + "settings.mqtt.label": "MQTT", + "settings.mqtt.hint": "配置 MQTT 代理连接,用于自动化条件和触发器。", + "settings.mqtt.enabled": "启用 MQTT", + "settings.mqtt.host_label": "代理主机", + "settings.mqtt.port_label": "端口", + "settings.mqtt.username_label": "用户名", + "settings.mqtt.password_label": "密码", + "settings.mqtt.password_set_hint": "已设置密码 — 留空以保留", + "settings.mqtt.client_id_label": "客户端 ID", + "settings.mqtt.base_topic_label": "基础主题", + "settings.mqtt.save": "保存 MQTT 设置", + "settings.mqtt.saved": "MQTT 设置已保存", + "settings.mqtt.save_error": "保存 MQTT 设置失败", + "settings.mqtt.error_host_required": "代理主机不能为空", + "settings.logs.label": "服务器日志", + "settings.logs.hint": "实时查看服务器日志。使用过滤器显示所需的日志级别。", + "settings.logs.connect": "连接", + "settings.logs.disconnect": "断开", + "settings.logs.clear": "清除", + "settings.logs.error": "日志查看器连接失败", + "settings.logs.filter.all": "所有级别", + "settings.logs.filter.info": "Info+", + "settings.logs.filter.warning": "Warning+", + "settings.logs.filter.error": "仅错误", + "settings.logs.filter.all_desc": "显示所有日志消息", + "settings.logs.filter.info_desc": "Info、警告和错误", + "settings.logs.filter.warning_desc": "仅警告和错误", + "settings.logs.filter.error_desc": "仅错误", + "device.error.power_off_failed": "关闭设备失败", + "device.error.remove_failed": "移除设备失败", + "device.error.settings_load_failed": "加载设备设置失败", + "device.error.brightness": "更新亮度失败", + "device.error.required": "请填写所有字段", + "device.error.update": "更新设备失败", + "device.error.save": "保存设置失败", + "device.error.clone_failed": "克隆设备失败", + "device_discovery.error.fill_all_fields": "请填写所有字段", + "device_discovery.added": "设备添加成功", + "device_discovery.error.add_failed": "添加设备失败", + "calibration.error.load_failed": "加载校准失败", + "calibration.error.css_load_failed": "加载色带源失败", + "calibration.error.test_toggle_failed": "切换测试边缘失败", + "calibration.error.save_failed": "保存校准失败", + "calibration.error.led_count_mismatch": "LED总数必须等于设备LED数量", + "calibration.error.led_count_exceeded": "校准的LED超过了LED总数", + "calibration.mode.simple": "简单", + "calibration.mode.advanced": "高级", + "calibration.switch_to_advanced": "切换到高级模式", + "calibration.advanced.title": "高级校准", + "calibration.advanced.switch_to_simple": "切换到简单模式", + "calibration.advanced.lines_title": "线段", + "calibration.advanced.canvas_hint": "拖动显示器重新排列。点击边缘选择线段。滚动缩放,拖动空白区域平移。", + "calibration.advanced.reset_view": "重置视图", + "calibration.advanced.line_properties": "线段属性", + "calibration.advanced.picture_source": "来源:", + "calibration.advanced.picture_source.hint": "此线段采样的图片来源(显示器)", + "calibration.advanced.edge": "边缘:", + "calibration.advanced.edge.hint": "从屏幕哪条边缘采样像素", + "calibration.advanced.led_count": "LED数:", + "calibration.advanced.led_count.hint": "映射到此线段的LED数量", + "calibration.advanced.span_start": "起始位置:", + "calibration.advanced.span_start.hint": "沿边缘开始采样的位置(0 = 起点,1 = 终点)。用于仅覆盖边缘的一部分。", + "calibration.advanced.span_end": "结束位置:", + "calibration.advanced.span_end.hint": "沿边缘结束采样的位置(0 = 起点,1 = 终点)。与起始位置一起定义活动区域。", + "calibration.advanced.border_width": "深度(像素):", + "calibration.advanced.border_width.hint": "从边缘向内采样多少像素。较大的值会捕获更多屏幕内部区域。", + "calibration.advanced.reverse": "反转", + "calibration.advanced.no_lines_warning": "请至少添加一条线段", + "dashboard.error.automation_toggle_failed": "切换自动化失败", + "dashboard.error.start_failed": "启动处理失败", + "dashboard.error.stop_failed": "停止处理失败", + "dashboard.error.stop_all": "停止所有目标失败", + "target.error.editor_open_failed": "打开目标编辑器失败", + "target.error.start_failed": "启动目标失败", + "target.error.stop_failed": "停止目标失败", + "target.error.clone_failed": "克隆目标失败", + "target.error.delete_failed": "删除目标失败", + "targets.stop_all.button": "全部停止", + "targets.stop_all.none_running": "当前没有运行中的目标", + "targets.stop_all.stopped": "已停止 {count} 个目标", + "targets.stop_all.error": "停止目标失败", + "audio_source.error.load": "加载音频源失败", + "audio_template.error.clone_failed": "克隆音频模板失败", + "value_source.error.load": "加载数值源失败", + "color_strip.error.editor_open_failed": "打开色带编辑器失败", + "color_strip.error.clone_failed": "克隆色带源失败", + "color_strip.error.delete_failed": "删除色带源失败", + "pattern.error.editor_open_failed": "打开图案模板编辑器失败", + "pattern.error.clone_failed": "克隆图案模板失败", + "pattern.error.delete_failed": "删除图案模板失败", + "pattern.error.capture_bg_failed": "捕获背景失败", + "stream.error.clone_picture_failed": "克隆图片源失败", + "stream.error.clone_capture_failed": "克隆捕获模板失败", + "stream.error.clone_pp_failed": "克隆后处理模板失败", + "theme.switched.dark": "已切换到深色主题", + "theme.switched.light": "已切换到浅色主题", + "accent.color.updated": "强调色已更新", + "search.footer": "↑↓ 导航 · Enter 选择 · Esc 关闭", + "sync_clock.group.title": "同步时钟", + "sync_clock.add": "添加同步时钟", + "sync_clock.edit": "编辑同步时钟", + "sync_clock.name": "名称:", + "sync_clock.name.placeholder": "主动画时钟", + "sync_clock.name.hint": "此同步时钟的描述性名称", + "sync_clock.speed": "速度:", + "sync_clock.speed.hint": "所有关联源的动画速度倍率。1.0 = 正常,2.0 = 双倍,0.5 = 半速。", + "sync_clock.description": "描述(可选):", + "sync_clock.description.placeholder": "可选描述", + "sync_clock.description.hint": "关于此时钟用途的可选备注", + "sync_clock.status.running": "运行中", + "sync_clock.status.paused": "已暂停", + "sync_clock.action.pause": "暂停", + "sync_clock.action.resume": "恢复", + "sync_clock.action.reset": "重置", + "sync_clock.error.name_required": "时钟名称为必填项", + "sync_clock.error.load": "加载同步时钟失败", + "sync_clock.created": "同步时钟已创建", + "sync_clock.updated": "同步时钟已更新", + "sync_clock.deleted": "同步时钟已删除", + "sync_clock.paused": "时钟已暂停", + "sync_clock.resumed": "时钟已恢复", + "sync_clock.reset_done": "时钟已重置为零", + "sync_clock.delete.confirm": "删除此同步时钟?关联的源将失去同步并以默认速度运行。", + "sync_clock.elapsed": "已用时间", + "weather_source.group.title": "天气源", + "weather_source.add": "添加天气源", + "weather_source.edit": "编辑天气源", + "weather_source.name": "名称:", + "weather_source.name.placeholder": "我的天气", + "weather_source.name.hint": "天气数据源的描述性名称", + "weather_source.provider": "提供商:", + "weather_source.provider.hint": "天气数据提供商。Open-Meteo免费且无需API密钥。", + "weather_source.provider.open_meteo.desc": "免费,无需API密钥", + "weather_source.location": "位置:", + "weather_source.location.hint": "您的地理坐标。使用自动检测按钮或手动输入。", + "weather_source.latitude": "纬度:", + "weather_source.longitude": "经度:", + "weather_source.use_my_location": "使用我的位置", + "weather_source.update_interval": "更新间隔:", + "weather_source.update_interval.hint": "获取天气数据的频率。值越低,更新越及时。", + "weather_source.description": "描述(可选):", + "weather_source.description.placeholder": "可选描述", + "weather_source.test": "测试", + "weather_source.error.name_required": "天气源名称为必填项", + "weather_source.error.load": "加载天气源失败", + "weather_source.created": "天气源已创建", + "weather_source.updated": "天气源已更新", + "weather_source.deleted": "天气源已删除", + "weather_source.delete.confirm": "删除此天气源?关联的色带源将失去天气数据。", + "weather_source.geo.success": "位置已检测", + "weather_source.geo.error": "地理定位失败", + "weather_source.geo.not_supported": "您的浏览器不支持地理定位", + "streams.group.weather": "天气", + "section.empty.weather_sources": "暂无天气源。点击+添加。", + "color_strip.clock": "同步时钟:", + "color_strip.clock.hint": "关联同步时钟以在多个源之间同步动画。速度在时钟上控制。", + "graph.title": "图表", + "graph.fit_all": "显示所有节点", + "graph.zoom_in": "放大", + "graph.zoom_out": "缩小", + "graph.search": "搜索节点", + "graph.search_placeholder": "搜索实体...", + "graph.legend": "图例", + "graph.minimap": "小地图", + "graph.relayout": "重新布局", + "graph.empty": "暂无实体", + "graph.empty.hint": "创建设备、源和目标后即可在此查看。", + "graph.disconnect": "断开连接", + "graph.connection_updated": "连接已更新", + "graph.connection_failed": "更新连接失败", + "graph.connection_removed": "连接已移除", + "graph.disconnect_failed": "断开连接失败", + "graph.relayout_confirm": "重置所有手动节点位置并重新布局图表?", + "graph.fullscreen": "切换全屏", + "graph.add_entity": "添加实体", + "graph.color_picker": "节点颜色", + "graph.filter": "筛选节点", + "graph.filter_placeholder": "按名称筛选...", + "graph.filter_clear": "清除筛选", + "graph.filter_running": "运行中", + "graph.filter_stopped": "已停止", + "graph.filter_types": "类型", + "graph.filter_group.capture": "捕获", + "graph.filter_group.strip": "色带", + "graph.filter_group.audio": "音频", + "graph.filter_group.targets": "目标", + "graph.filter_group.other": "其他", + "graph.bulk_delete_confirm": "删除 {count} 个选中的实体?", + "graph.nothing_to_undo": "没有可撤销的操作", + "graph.nothing_to_redo": "没有可重做的操作", + "graph.help_title": "键盘快捷键", + "graph.help.search": "搜索", + "graph.help.filter": "筛选", + "graph.help.add": "添加实体", + "graph.help.shortcuts": "快捷键", + "graph.help.delete": "删除 / 断开", + "graph.help.select_all": "全选", + "graph.help.undo": "撤销", + "graph.help.redo": "重做", + "graph.help.fullscreen": "全屏", + "graph.help.deselect": "取消选择", + "graph.help.navigate": "节点导航", + "graph.help.click": "单击", + "graph.help.click_desc": "选择节点", + "graph.help.dblclick": "双击", + "graph.help.dblclick_desc": "缩放到节点", + "graph.help.shift_click": "Shift+单击", + "graph.help.shift_click_desc": "多选", + "graph.help.shift_drag": "Shift+拖拽", + "graph.help.shift_drag_desc": "框选", + "graph.help.drag_node": "拖拽节点", + "graph.help.drag_node_desc": "重新定位", + "graph.help.drag_port": "拖拽端口", + "graph.help.drag_port_desc": "连接实体", + "graph.help.right_click": "右键边线", + "graph.help.right_click_desc": "断开连接", + "graph.tooltip.fps": "帧率", + "graph.tooltip.errors": "错误", + "graph.tooltip.uptime": "运行时间", + "automation.enabled": "自动化已启用", + "automation.disabled": "自动化已禁用", + "scene_preset.activated": "预设已激活", + "scene_preset.used_by": "被 %d 个自动化使用", + "settings.api_keys.label": "API 密钥", + "settings.api_keys.hint": "API 密钥在服务器配置文件 (config.yaml) 中定义。编辑文件并重启服务器以应用更改。", + "settings.api_keys.empty": "未配置 API 密钥", + "settings.api_keys.load_error": "加载 API 密钥失败", + "settings.partial.label": "部分导出 / 导入", + "settings.partial.hint": "导出或导入单个实体类型。导入会替换或合并现有数据并重启服务器。", + "settings.partial.store.devices": "设备", + "settings.partial.store.output_targets": "LED 目标", + "settings.partial.store.color_strip_sources": "色带", + "settings.partial.store.picture_sources": "图像源", + "settings.partial.store.audio_sources": "音频源", + "settings.partial.store.audio_templates": "音频模板", + "settings.partial.store.capture_templates": "捕获模板", + "settings.partial.store.postprocessing_templates": "后处理模板", + "settings.partial.store.color_strip_processing_templates": "CSS 处理模板", + "settings.partial.store.pattern_templates": "图案模板", + "settings.partial.store.value_sources": "值源", + "settings.partial.store.sync_clocks": "同步时钟", + "settings.partial.store.automations": "自动化", + "settings.partial.store.scene_presets": "场景预设", + "settings.partial.export_button": "导出", + "settings.partial.import_button": "从文件导入", + "settings.partial.merge_label": "合并(添加/覆盖,保留现有)", + "settings.partial.export_success": "导出成功", + "settings.partial.export_error": "导出失败", + "settings.partial.import_success": "导入成功", + "settings.partial.import_error": "导入失败", + "settings.partial.import_confirm_replace": "这将替换所有 {store} 数据并重启服务器。继续吗?", + "settings.partial.import_confirm_merge": "这将合并 {store} 数据并重启服务器。继续吗?", + "section.empty.devices": "暂无设备。点击 + 添加。", + "section.empty.targets": "暂无 LED 目标。点击 + 添加。", + "section.empty.kc_targets": "暂无键色目标。点击 + 添加。", + "section.empty.pattern_templates": "暂无图案模板。点击 + 添加。", + "section.empty.picture_sources": "暂无源。点击 + 添加。", + "section.empty.capture_templates": "暂无捕获模板。点击 + 添加。", + "section.empty.pp_templates": "暂无后处理模板。点击 + 添加。", + "section.empty.audio_sources": "暂无音频源。点击 + 添加。", + "section.empty.audio_templates": "暂无音频模板。点击 + 添加。", + "section.empty.color_strips": "暂无色带。点击 + 添加。", + "section.empty.value_sources": "暂无值源。点击 + 添加。", + "section.empty.sync_clocks": "暂无同步时钟。点击 + 添加。", + "section.empty.cspt": "暂无 CSS 处理模板。点击 + 添加。", + "section.empty.automations": "暂无自动化。点击 + 添加。", + "section.empty.scenes": "暂无场景预设。点击 + 添加。", + "bulk.select": "选择", + "bulk.cancel": "取消", + "bulk.selected_count.one": "已选 {count} 项", + "bulk.selected_count.other": "已选 {count} 项", + "bulk.select_all": "全选", + "bulk.deselect_all": "取消全选", + "bulk.delete": "删除", + "bulk.start": "启动", + "bulk.stop": "停止", + "bulk.enable": "启用", + "bulk.disable": "禁用", + "bulk.confirm_delete.one": "删除 {count} 项?", + "bulk.confirm_delete.other": "删除 {count} 项?", + "appearance.style.label": "样式预设", + "appearance.style.hint": "选择一个视觉主题 — 字体和配色方案一起应用。", + "appearance.preset.default": "默认", + "appearance.preset.midnight": "午夜", + "appearance.preset.ember": "余烬", + "appearance.preset.arctic": "极地", + "appearance.preset.terminal": "终端", + "appearance.preset.neon": "霓虹", + "appearance.preset.sakura": "樱花", + "appearance.preset.ocean": "海洋", + "appearance.preset.copper": "铜色", + "appearance.preset.vapor": "蒸汽波", + "appearance.preset.monolith": "黑白", + "appearance.preset.applied": "样式已应用", + "appearance.bg.label": "背景效果", + "appearance.bg.hint": "在界面后面添加环境背景层。", + "appearance.bg.none": "无", + "appearance.bg.noise": "噪声场", + "appearance.bg.aurora": "极光", + "appearance.bg.plasma": "等离子", + "appearance.bg.rain": "数字雨", + "appearance.bg.stars": "星空", + "appearance.bg.warp": "隧道", + "appearance.bg.grid": "点阵", + "appearance.bg.mesh": "渐变网格", + "appearance.bg.scanlines": "扫描线", + "appearance.bg.applied": "背景效果已应用", + "settings.tab.updates": "更新", + "settings.tab.about": "关于", + "update.status_label": "更新状态", + "update.current_version": "当前版本:", + "update.badge_tooltip": "有新版本可用 — 点击查看详情", + "update.available": "版本 {version} 可用", + "update.up_to_date": "已是最新版本", + "update.prerelease": "预发布", + "update.view_release": "查看发布", + "update.dismiss": "忽略", + "update.check_now": "检查更新", + "update.check_error": "检查更新失败", + "update.last_check": "上次检查", + "update.never": "从未", + "update.release_notes": "发布说明", + "update.view_release_notes": "查看发布说明", + "update.auto_check_label": "自动检查设置", + "update.auto_check_hint": "在后台定期检查新版本。", + "update.enable": "启用自动检查", + "update.interval_label": "检查间隔", + "update.channel_label": "频道", + "update.channel.stable": "稳定版", + "update.channel.stable_desc": "仅稳定版本", + "update.channel.prerelease": "预发布", + "update.channel.prerelease_desc": "包括 alpha、beta 和 RC 版本", + "update.save_settings": "保存设置", + "update.settings_saved": "更新设置已保存", + "update.settings_save_error": "保存更新设置失败", + "update.apply_now": "立即更新", + "update.apply_confirm": "下载并安装版本 {version}?服务器将自动重启。", + "update.apply_error": "更新失败", + "update.applying": "正在应用更新…", + "update.downloading": "正在下载…", + "update.install_type_label": "安装类型:", + "update.install_type.installer": "Windows 安装程序", + "update.install_type.portable": "便携版", + "update.install_type.docker": "Docker", + "update.install_type.dev": "开发环境", + "color_strip.notification.search_apps": "搜索通知应用…", + "asset.group.title": "资源", + "asset.upload": "上传资源", + "asset.edit": "编辑资源", + "asset.name": "名称:", + "asset.name.hint": "资源的显示名称。", + "asset.description": "描述:", + "asset.description.hint": "资源的可选描述。", + "asset.file": "文件:", + "asset.file.hint": "选择要上传的文件(声音、图片、视频或其他)。", + "asset.drop_or_browse": "拖放文件到此处或点击浏览", + "asset.uploaded": "资源已上传", + "asset.updated": "资源已更新", + "asset.deleted": "资源已删除", + "asset.confirm_delete": "删除此资源?", + "asset.error.name_required": "名称为必填项", + "asset.error.no_file": "请选择要上传的文件", + "asset.error.delete_failed": "删除资源失败", + "asset.error.play_failed": "播放声音失败", + "asset.error.download_failed": "下载资源失败", + "asset.play": "播放", + "asset.download": "下载", + "asset.prebuilt": "内置", + "asset.prebuilt_restored": "已恢复 {count} 个内置资源", + "asset.prebuilt_none_to_restore": "所有内置资源均已可用", + "asset.restore_prebuilt": "恢复内置声音", + "asset.type.sound": "声音", + "asset.type.image": "图片", + "asset.type.video": "视频", + "asset.type.other": "其他", + "streams.group.assets": "资源", + "section.empty.assets": "暂无资源。点击 + 上传一个。", + "donation.message": "LedGrab 是免费开源软件。如果它对您有帮助,请考虑支持开发。", + "donation.support": "支持项目", + "donation.view_source": "查看源代码", + "donation.later": "稍后提醒", + "donation.dismiss": "不再显示", + "donation.about_title": "关于 LedGrab", + "donation.about_opensource": "LedGrab 是开源软件,可免费使用和修改。", + "donation.about_donate": "支持开发", + "donation.about_license": "MIT 许可证" +} diff --git a/server/src/wled_controller/storage/key_colors_output_target.py b/server/src/wled_controller/storage/key_colors_output_target.py deleted file mode 100644 index 292102e..0000000 --- a/server/src/wled_controller/storage/key_colors_output_target.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Key colors output target — extracts key colors from image rectangles.""" - -from dataclasses import dataclass, field -from datetime import datetime, timezone - -from wled_controller.storage.output_target import OutputTarget -from wled_controller.storage.utils import resolve_ref - - -@dataclass -class KeyColorRectangle: - """A named rectangle in relative coordinates (0.0 to 1.0).""" - - name: str - x: float - y: float - width: float - height: float - - def to_dict(self) -> dict: - return { - "name": self.name, - "x": self.x, - "y": self.y, - "width": self.width, - "height": self.height, - } - - @classmethod - def from_dict(cls, data: dict) -> "KeyColorRectangle": - return cls( - name=data["name"], - x=float(data.get("x", 0.0)), - y=float(data.get("y", 0.0)), - width=float(data.get("width", 1.0)), - height=float(data.get("height", 1.0)), - ) - - -@dataclass -class KeyColorsSettings: - """Settings for key colors extraction.""" - - fps: int = 10 - interpolation_mode: str = "average" - smoothing: float = 0.3 - pattern_template_id: str = "" - brightness: float = 1.0 - brightness_value_source_id: str = "" - - def to_dict(self) -> dict: - return { - "fps": self.fps, - "interpolation_mode": self.interpolation_mode, - "smoothing": self.smoothing, - "pattern_template_id": self.pattern_template_id, - "brightness": self.brightness, - "brightness_value_source_id": self.brightness_value_source_id, - } - - @classmethod - def from_dict(cls, data: dict) -> "KeyColorsSettings": - return cls( - fps=data.get("fps", 10), - interpolation_mode=data.get("interpolation_mode", "average"), - smoothing=data.get("smoothing", 0.3), - pattern_template_id=data.get("pattern_template_id", ""), - brightness=data.get("brightness", 1.0), - brightness_value_source_id=data.get("brightness_value_source_id", ""), - ) - - -@dataclass -class KeyColorsOutputTarget(OutputTarget): - """Key colors extractor target — extracts key colors from image rectangles.""" - - picture_source_id: str = "" - settings: KeyColorsSettings = field(default_factory=KeyColorsSettings) - - def register_with_manager(self, manager) -> None: - """Register this KC target with the processor manager.""" - manager.add_kc_target( - target_id=self.id, - picture_source_id=self.picture_source_id, - settings=self.settings, - ) - - def sync_with_manager(self, manager, *, settings_changed: bool, - source_changed: bool = False, - css_changed: bool = False, - device_changed: bool = False, - brightness_vs_changed: bool = False) -> None: - """Push changed fields to the processor manager.""" - if settings_changed: - manager.update_target_settings(self.id, self.settings) - if source_changed: - manager.update_target_source(self.id, self.picture_source_id) - if brightness_vs_changed: - manager.update_target_brightness_vs(self.id, self.settings.brightness_value_source_id) - - def update_fields(self, *, name=None, device_id=None, picture_source_id=None, - settings=None, key_colors_settings=None, description=None, - tags=None, - **_kwargs) -> None: - """Apply mutable field updates for KC targets.""" - super().update_fields(name=name, description=description, tags=tags) - if picture_source_id is not None: - self.picture_source_id = resolve_ref(picture_source_id, self.picture_source_id) - if key_colors_settings is not None: - self.settings = key_colors_settings - - @property - def has_picture_source(self) -> bool: - return True - - def to_dict(self) -> dict: - d = super().to_dict() - d["picture_source_id"] = self.picture_source_id - d["settings"] = self.settings.to_dict() - return d - - @classmethod - def from_dict(cls, data: dict) -> "KeyColorsOutputTarget": - settings_data = data.get("settings", {}) - settings = KeyColorsSettings.from_dict(settings_data) - - return cls( - id=data["id"], - name=data["name"], - target_type="key_colors", - picture_source_id=data.get("picture_source_id", ""), - settings=settings, - description=data.get("description"), - tags=data.get("tags", []), - created_at=datetime.fromisoformat(data.get("created_at", datetime.now(timezone.utc).isoformat())), - updated_at=datetime.fromisoformat(data.get("updated_at", datetime.now(timezone.utc).isoformat())), - ) diff --git a/server/src/wled_controller/storage/output_target.py b/server/src/wled_controller/storage/output_target.py index 70e0f97..b9a7cf8 100644 --- a/server/src/wled_controller/storage/output_target.py +++ b/server/src/wled_controller/storage/output_target.py @@ -11,7 +11,7 @@ class OutputTarget: id: str name: str - target_type: str # "wled", "key_colors", ... + target_type: str # "led", "ha_light" created_at: datetime updated_at: datetime description: Optional[str] = None @@ -32,9 +32,6 @@ class OutputTarget: *, name=None, device_id=None, - picture_source_id=None, - settings=None, - key_colors_settings=None, description=None, tags: Optional[List[str]] = None, **_kwargs, @@ -47,11 +44,6 @@ class OutputTarget: if tags is not None: self.tags = tags - @property - def has_picture_source(self) -> bool: - """Whether this target type uses a picture source.""" - return False - def to_dict(self) -> dict: """Convert to dictionary.""" return { diff --git a/server/src/wled_controller/storage/pattern_template.py b/server/src/wled_controller/storage/pattern_template.py index b20eb03..73d3bf4 100644 --- a/server/src/wled_controller/storage/pattern_template.py +++ b/server/src/wled_controller/storage/pattern_template.py @@ -4,7 +4,35 @@ from dataclasses import dataclass, field from datetime import datetime, timezone from typing import List, Optional -from wled_controller.storage.key_colors_output_target import KeyColorRectangle + +@dataclass +class KeyColorRectangle: + """A named rectangle in relative coordinates (0.0 to 1.0).""" + + name: str + x: float + y: float + width: float + height: float + + def to_dict(self) -> dict: + return { + "name": self.name, + "x": self.x, + "y": self.y, + "width": self.width, + "height": self.height, + } + + @classmethod + def from_dict(cls, data: dict) -> "KeyColorRectangle": + return cls( + name=data["name"], + x=float(data.get("x", 0.0)), + y=float(data.get("y", 0.0)), + width=float(data.get("width", 1.0)), + height=float(data.get("height", 1.0)), + ) @dataclass @@ -39,12 +67,16 @@ class PatternTemplate: id=data["id"], name=data["name"], rectangles=rectangles, - created_at=datetime.fromisoformat(data["created_at"]) - if isinstance(data.get("created_at"), str) - else data.get("created_at", datetime.now(timezone.utc)), - updated_at=datetime.fromisoformat(data["updated_at"]) - if isinstance(data.get("updated_at"), str) - else data.get("updated_at", datetime.now(timezone.utc)), + created_at=( + datetime.fromisoformat(data["created_at"]) + if isinstance(data.get("created_at"), str) + else data.get("created_at", datetime.now(timezone.utc)) + ), + updated_at=( + datetime.fromisoformat(data["updated_at"]) + if isinstance(data.get("updated_at"), str) + else data.get("updated_at", datetime.now(timezone.utc)) + ), description=data.get("description"), tags=data.get("tags", []), ) diff --git a/server/src/wled_controller/storage/pattern_template_store.py b/server/src/wled_controller/storage/pattern_template_store.py index 610ba13..f6bb64b 100644 --- a/server/src/wled_controller/storage/pattern_template_store.py +++ b/server/src/wled_controller/storage/pattern_template_store.py @@ -6,8 +6,7 @@ from typing import List, Optional from wled_controller.storage.base_sqlite_store import BaseSqliteStore from wled_controller.storage.database import Database -from wled_controller.storage.key_colors_output_target import KeyColorRectangle -from wled_controller.storage.pattern_template import PatternTemplate +from wled_controller.storage.pattern_template import KeyColorRectangle, PatternTemplate from wled_controller.utils import get_logger logger = get_logger(__name__) @@ -113,10 +112,5 @@ class PatternTemplateStore(BaseSqliteStore[PatternTemplate]): return template def get_targets_referencing(self, template_id: str, output_target_store) -> List[str]: - """Return names of KC targets that reference this template.""" - from wled_controller.storage.key_colors_output_target import KeyColorsOutputTarget - - return [ - target.name for target in output_target_store.get_all_targets() - if isinstance(target, KeyColorsOutputTarget) and target.settings.pattern_template_id == template_id - ] + """Return names of targets that reference this template (legacy, always empty).""" + return [] diff --git a/server/src/wled_controller/templates/modals/kc-editor.html b/server/src/wled_controller/templates/modals/kc-editor.html deleted file mode 100644 index be048ae..0000000 --- a/server/src/wled_controller/templates/modals/kc-editor.html +++ /dev/null @@ -1,92 +0,0 @@ - - diff --git a/server/tests/storage/test_output_target_store.py b/server/tests/storage/test_output_target_store.py index 3d5c96a..32949f1 100644 --- a/server/tests/storage/test_output_target_store.py +++ b/server/tests/storage/test_output_target_store.py @@ -1,4 +1,4 @@ -"""Tests for OutputTargetStore — CRUD for LED and key_colors targets.""" +"""Tests for OutputTargetStore — CRUD for LED and HA light targets.""" import pytest @@ -34,18 +34,6 @@ class TestOutputTargetModel: assert isinstance(target, WledOutputTarget) assert target.device_id == "dev_1" - def test_key_colors_type_rejected(self): - """key_colors target type removed — from_dict raises ValueError.""" - data = { - "id": "pt_2", - "name": "KC Target", - "target_type": "key_colors", - "created_at": "2025-01-01T00:00:00+00:00", - "updated_at": "2025-01-01T00:00:00+00:00", - } - with pytest.raises(ValueError, match="Unknown target type"): - OutputTarget.from_dict(data) - def test_unknown_type_raises(self): data = { "id": "pt_3", @@ -78,11 +66,6 @@ class TestOutputTargetStoreCRUD: assert t.name == "LED 1" assert store.count() == 1 - def test_create_key_colors_target_rejected(self, store): - """key_colors target type is no longer supported (migrated to CSS source).""" - with pytest.raises(ValueError, match="Invalid target type"): - store.create_target(name="KC 1", target_type="key_colors") - def test_create_invalid_type(self, store): with pytest.raises(ValueError, match="Invalid target type"): store.create_target(name="Bad", target_type="invalid")