Загрузка данных


import classNames from 'classnames';
import { type ChangeEvent, type PointerEvent, useId, useRef } from 'react';

import styles from './index.module.scss';

interface CommonRangeFieldProps {
  label?: string;
  min?: number;
  max?: number;
  step?: number;
  color?: string;
  suffix?: string;
  size?: 'sm' | 'md';
  disabled?: boolean;
}

interface SingleRangeFieldProps extends CommonRangeFieldProps {
  mode?: 'single';
  value: number;
  onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}

interface IntervalRangeFieldProps extends CommonRangeFieldProps {
  mode: 'range';
  value: [number, number];
  marks?: readonly number[];
  onRangeChange: (value: [number, number]) => void;
}

interface RangeDragState {
  mode: 'range' | 'from' | 'to';
  pointerId: number;
  pointerX: number;
  from: number;
  to: number;
  trackWidth: number;
}

type RangeFieldProps = SingleRangeFieldProps | IntervalRangeFieldProps;

export const RangeField = (props: RangeFieldProps) => {
  const {
    label,
    min = 0,
    max = 100,
    step = 1,
    color = 'var(--blue-1)',
    suffix = '%',
    size = 'md',
    disabled = false,
  } = props;

  const inputId = useId();
  const trackRef = useRef<HTMLDivElement>(null);
  const dragRef = useRef<RangeDragState | null>(null);

  if (props.mode !== 'range') {
    return (
      <div
        className={classNames(styles.range, {
          [styles.range_sm]: size === 'sm',
        })}
      >
        {label && (
          <label
            htmlFor={inputId}
            className={styles.range_label}
          >
            {label}
          </label>
        )}

        <div className={styles.range_wrapper}>
          <input
            id={inputId}
            type="range"
            min={min}
            max={max}
            step={step}
            value={props.value}
            className={styles.range_input}
            style={{ ['--range-color' as string]: color }}
            onChange={props.onChange}
            disabled={disabled}
          />

          <span className={styles.range_value}>
            {props.value}
            {suffix}
          </span>
        </div>
      </div>
    );
  }

  const {
    value: [from, to],
    marks = [],
    onRangeChange,
  } = props;

  const span = max - min;
  const left = span > 0 ? ((from - min) / span) * 100 : 0;
  const right = span > 0 ? ((to - min) / span) * 100 : 0;
  const isDisabled = disabled || span <= 0;

  const scaleMarks =
    span > 0
      ? [
          min,
          ...Array.from(new Set(marks))
            .filter((value) => value > min && value < max)
            .sort((a, b) => a - b),
          max,
        ]
      : [min];

  const handleDragEnd = (event: PointerEvent<HTMLElement>) => {
    if (dragRef.current?.pointerId === event.pointerId) {
      dragRef.current = null;
    }
  };

  const handleDragStart = (event: PointerEvent<HTMLElement>, mode: RangeDragState['mode']) => {
    if (isDisabled || event.button !== 0 || dragRef.current || !trackRef.current) return;

    const { width } = trackRef.current.getBoundingClientRect();

    if (width <= 0) return;

    event.preventDefault();
    event.currentTarget.setPointerCapture(event.pointerId);

    dragRef.current = {
      mode,
      pointerId: event.pointerId,
      pointerX: event.clientX,
      from,
      to,
      trackWidth: width,
    };
  };

  const handleDragMove = (event: PointerEvent<HTMLElement>) => {
    const drag = dragRef.current;

    if (!drag || drag.pointerId !== event.pointerId || isDisabled) return;

    const pointerOffset = ((event.clientX - drag.pointerX) / drag.trackWidth) * span;
    const steppedOffset = Math.round(pointerOffset / step) * step;
    let nextFrom = drag.from;
    let nextTo = drag.to;

    if (drag.mode === 'range') {
      const offset = Math.max(min - drag.from, Math.min(max - drag.to, steppedOffset));

      nextFrom += offset;
      nextTo += offset;
    } else if (drag.from === drag.to) {
      const value = Math.max(min, Math.min(max, drag.from + steppedOffset));

      nextFrom = Math.min(drag.from, value);
      nextTo = Math.max(drag.to, value);
    } else if (drag.mode === 'from') {
      nextFrom = Math.max(min, Math.min(drag.to, drag.from + steppedOffset));
    } else {
      nextTo = Math.max(drag.from, Math.min(max, drag.to + steppedOffset));
    }

    if (nextFrom !== from || nextTo !== to) {
      onRangeChange([nextFrom, nextTo]);
    }
  };

  const handleTrackPointerDown = (event: PointerEvent<HTMLDivElement>) => {
    if (
      isDisabled ||
      from !== to ||
      event.button !== 0 ||
      event.defaultPrevented ||
      event.target instanceof HTMLInputElement ||
      !trackRef.current
    ) {
      return;
    }

    const { left: trackLeft, width } = trackRef.current.getBoundingClientRect();

    if (width <= 0) return;

    const value = min + Math.round((((event.clientX - trackLeft) / width) * span) / step) * step;
    const next = Math.max(min, Math.min(max, value));

    event.preventDefault();

    if (next !== from) {
      onRangeChange([Math.min(from, next), Math.max(to, next)]);
    }
  };

  const handleFromChange = (event: ChangeEvent<HTMLInputElement>) => {
    if (dragRef.current) return;

    onRangeChange([Math.min(event.currentTarget.valueAsNumber, to), to]);
  };

  const handleToChange = (event: ChangeEvent<HTMLInputElement>) => {
    if (dragRef.current) return;

    onRangeChange([from, Math.max(event.currentTarget.valueAsNumber, from)]);
  };

  const dragHandlers = {
    onPointerMove: handleDragMove,
    onPointerUp: handleDragEnd,
    onPointerCancel: handleDragEnd,
    onLostPointerCapture: handleDragEnd,
  };

  return (
    <div
      className={classNames(styles.range, styles.range_interval, {
        [styles.range_sm]: size === 'sm',
        [styles.range_disabled]: isDisabled,
        [styles.range_collapsed]: from === to,
      })}
    >
      {label && <span className={styles.range_label}>{label}</span>}

      <div
        ref={trackRef}
        className={styles.range_track}
        style={{ ['--range-color' as string]: color }}
        onPointerDown={handleTrackPointerDown}
      >
        <div className={styles.range_rail} />

        <div
          className={styles.range_selection}
          style={{
            left: `${left}%`,
            width: `${right - left}%`,
          }}
          onPointerDown={(event) => handleDragStart(event, 'range')}
          {...dragHandlers}
        />

        <input
          id={`${inputId}-from`}
          type="range"
          min={min}
          max={max}
          step={step}
          value={from}
          disabled={isDisabled}
          className={classNames(styles.range_handle, styles.range_handle_start, {
            [styles.range_handle_behind]: from === to && from === min,
          })}
          onChange={handleFromChange}
          onPointerDown={(event) => handleDragStart(event, 'from')}
          {...dragHandlers}
        />

        <input
          id={`${inputId}-to`}
          type="range"
          min={min}
          max={max}
          step={step}
          value={to}
          disabled={isDisabled}
          className={classNames(styles.range_handle, styles.range_handle_end)}
          onChange={handleToChange}
          onPointerDown={(event) => handleDragStart(event, 'to')}
          {...dragHandlers}
        />

        <button
          type="button"
          tabIndex={-1}
          className={styles.range_dragHandle}
          style={{ left: `${(left + right) / 2}%` }}
          disabled={isDisabled}
          onPointerDown={(event) => handleDragStart(event, 'range')}
          {...dragHandlers}
        >
          <svg
            width="12"
            height="8"
            viewBox="0 0 12 8"
            fill="currentColor"
          >
            <circle
              cx="2"
              cy="2"
              r="1"
            />
            <circle
              cx="6"
              cy="2"
              r="1"
            />
            <circle
              cx="10"
              cy="2"
              r="1"
            />
            <circle
              cx="2"
              cy="6"
              r="1"
            />
            <circle
              cx="6"
              cy="6"
              r="1"
            />
            <circle
              cx="10"
              cy="6"
              r="1"
            />
          </svg>
        </button>
      </div>

      <div className={styles.range_scale}>
        {scaleMarks.map((value) => (
          <span
            key={value}
            className={styles.range_scale_label}
            style={{
              left: `${span > 0 ? ((value - min) / span) * 100 : 0}%`,
            }}
          >
            {value}
            {suffix}
          </span>
        ))}
      </div>
    </div>
  );
};

















import classNames from 'classnames';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';

import { Button } from '@src/components/Button';

import { GearIcon, LockIcon, LockOpenIcon, TrashIcon } from '@src/components/Icon';
import { ToolbarColorControl } from '@src/components/ToolbarColorControl';
import { Tooltip } from '@src/components/Tooltip';
import { t } from '@src/translations';

import styles from './index.module.scss';

import type { Drawing } from '@core/Drawings';
import type { SettingsValues } from '@src/types/settings';
import type { PointerEvent, SyntheticEvent } from 'react';
import type { Observable } from 'rxjs';

interface FloatingDrawingToolbarProps {
  selectedDrawing$: Observable<Drawing | null>;
  onUpdateSettings: (settings: SettingsValues) => void;
  onToggleLock: () => void;
  onOpenSettings: () => void;
  onDelete: () => void;
}

interface Position {
  x: number;
  y: number;
}

interface DragState {
  pointerId: number;
  startX: number;
  startY: number;
  initialX: number;
  initialY: number;
}

const TOOLBAR_TOP_OFFSET = 12;

export function FloatingDrawingToolbar({
  selectedDrawing$,
  onUpdateSettings,
  onToggleLock,
  onOpenSettings,
  onDelete,
}: FloatingDrawingToolbarProps) {
  const toolbarRef = useRef<HTMLDivElement | null>(null);
  const dragStateRef = useRef<DragState | null>(null);
  const positionRef = useRef<Position | null>(null);

  const [selectedDrawing, setSelectedDrawing] = useState<Drawing | null>(null);
  const [settings, setSettings] = useState<SettingsValues>({});
  const [isLocked, setIsLocked] = useState(false);
  const [isDragging, setIsDragging] = useState(false);
  const [position, setPosition] = useState<Position | null>(null);

  const shouldShowToolbar = selectedDrawing?.hasSettings() ?? false;

  useEffect(() => {
    const subscription = selectedDrawing$.subscribe(setSelectedDrawing);

    return () => {
      subscription.unsubscribe();
    };
  }, [selectedDrawing$]);

  useEffect(() => {
    if (!selectedDrawing || !shouldShowToolbar) {
      setIsLocked(false);

      return;
    }

    const subscription = selectedDrawing.subscribeIsLocked(setIsLocked);

    return () => {
      subscription.unsubscribe();
    };
  }, [selectedDrawing, shouldShowToolbar]);

  useEffect(() => {
    if (!selectedDrawing || !shouldShowToolbar) {
      setSettings({});

      return;
    }

    const subscription = selectedDrawing.subscribeSettings(setSettings);

    return () => {
      subscription.unsubscribe();
    };
  }, [selectedDrawing, shouldShowToolbar]);

  useLayoutEffect(() => {
    if (!selectedDrawing || !shouldShowToolbar) {
      return;
    }

    const toolbar = toolbarRef.current;
    const container = toolbar?.parentElement;

    if (!toolbar || !container) {
      return;
    }

    const currentPosition = positionRef.current;

    if (currentPosition) {
      updatePosition(clampPosition(currentPosition.x, currentPosition.y, toolbar, container));

      return;
    }

    updatePosition({
      x: Math.round((container.clientWidth - toolbar.offsetWidth) / 2),
      y: TOOLBAR_TOP_OFFSET,
    });
  }, [selectedDrawing, shouldShowToolbar]);

  const handleDragStart = (event: PointerEvent<HTMLButtonElement>): void => {
    const currentPosition = positionRef.current;

    if (event.button !== 0 || !currentPosition) {
      return;
    }

    event.preventDefault();
    event.currentTarget.setPointerCapture(event.pointerId);

    dragStateRef.current = {
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      initialX: currentPosition.x,
      initialY: currentPosition.y,
    };

    setIsDragging(true);
  };

  const handleDrag = (event: PointerEvent<HTMLButtonElement>): void => {
    const dragState = dragStateRef.current;
    const toolbar = toolbarRef.current;
    const container = toolbar?.parentElement;

    if (!dragState || dragState.pointerId !== event.pointerId || !toolbar || !container) {
      return;
    }

    event.preventDefault();

    updatePosition(
      clampPosition(
        dragState.initialX + event.clientX - dragState.startX,
        dragState.initialY + event.clientY - dragState.startY,
        toolbar,
        container,
      ),
    );
  };

  const handleDragEnd = (event: PointerEvent<HTMLButtonElement>): void => {
    if (dragStateRef.current?.pointerId !== event.pointerId) {
      return;
    }

    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId);
    }

    dragStateRef.current = null;
    setIsDragging(false);
  };

  const stopPropagation = (event: SyntheticEvent): void => {
    event.stopPropagation();
  };

  function updatePosition(nextPosition: Position): void {
    positionRef.current = nextPosition;

    setPosition((currentPosition) => {
      if (currentPosition?.x === nextPosition.x && currentPosition.y === nextPosition.y) {
        return currentPosition;
      }

      return nextPosition;
    });
  }

  if (!selectedDrawing || !shouldShowToolbar) {
    return null;
  }

  const toolbarSettings = selectedDrawing.getToolbarSettings();
  const lockLabel = isLocked ? t('Unlock') : t('Lock');

  return (
    <div
      ref={toolbarRef}
      className={styles.toolbar}
      style={{
        visibility: position ? 'visible' : 'hidden',
        transform: `translate3d(${position?.x ?? 0}px, ${position?.y ?? 0}px, 0)`,
      }}
      onPointerDown={stopPropagation}
      onClick={stopPropagation}
      onDoubleClick={stopPropagation}
      onContextMenu={stopPropagation}
    >
      <button
        type="button"
        className={classNames(styles.toolbar_handle, {
          [styles.dragging]: isDragging,
        })}
        onPointerDown={handleDragStart}
        onPointerMove={handleDrag}
        onPointerUp={handleDragEnd}
        onPointerCancel={handleDragEnd}
      >
        <span />
        <span />
        <span />
        <span />
        <span />
        <span />
      </button>

      {toolbarSettings.map((field) => {
        if (field.type !== 'color' || field.toolbar.control !== 'color') {
          return null;
        }

        return (
          <ToolbarColorControl
            key={field.key}
            role={field.toolbar.role}
            value={String(settings[field.key] ?? field.defaultValue)}
            onChange={(value) => {
              onUpdateSettings({
                [field.key]: value,
              });
            }}
          />
        );
      })}

      <Tooltip
        showMessageOnFocus
        label={t('Settings')}
        location="top"
      >
        <Button
          onClick={onOpenSettings}
          label={<GearIcon />}
        />
      </Tooltip>

      <Tooltip
        showMessageOnFocus
        label={lockLabel}
        location="top"
      >
        <Button
          pressed={isLocked}
          onClick={onToggleLock}
          label={isLocked ? <LockIcon /> : <LockOpenIcon />}
        />
      </Tooltip>

      <Tooltip
        showMessageOnFocus
        label={t('Remove')}
        location="top"
      >
        <Button
          onClick={onDelete}
          label={<TrashIcon />}
        />
      </Tooltip>
    </div>
  );
}

function clampPosition(x: number, y: number, toolbar: HTMLElement, container: HTMLElement): Position {
  return {
    x: Math.max(0, Math.min(Math.round(x), container.clientWidth - toolbar.offsetWidth)),
    y: Math.max(0, Math.min(Math.round(y), container.clientHeight - toolbar.offsetHeight)),
  };
}

















$handle-size: 3px;

.toolbar {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  border-radius: var(--space-0250);
  background-color: var(--neutral-2);
  pointer-events: auto;
  user-select: none;
  touch-action: none;
  white-space: nowrap;

  &_handle {
    display: grid;
    grid-template-columns: repeat(2, $handle-size);
    gap: $handle-size;
    align-content: center;
    justify-content: center;
    align-self: stretch;
    width: var(--space-1500);
    padding: 0;
    border: 0;
    background-color: transparent;
    cursor: grab;
    touch-action: none;

    &:active {
      cursor: grabbing;
    }

    span {
      width: var(--space-0125);
      height: var(--space-0125);
      border-radius: 50%;
      background-color: var(--neutral-700);
    }
  }
}

















export const russian = {
  Indicators: 'Индикаторы',
  Settings: 'Настройки',
  'Add volume indicator': 'Добавить индикатор объёма',
  tick: 'тик',
  ticks: 'тики',
  tick_few: 'тика',
  tick_many: 'тиков',
  second: 'секунда',
  seconds: 'секунды',
  second_few: 'секунды',
  second_many: 'секунд',
  minute: 'минута',
  minutes: 'минуты',
  minute_few: 'минуты',
  minute_many: 'минут',
  hour: 'час',
  hours: 'часы',
  hour_few: 'часа',
  hour_many: 'часов',
  day: 'день',
  days: 'дни',
  day_few: 'дня',
  day_many: 'дней',
  week: 'неделя',
  weeks: 'недели',
  week_few: 'недели',
  week_many: 'недель',
  month: 'месяц',
  months: 'месяцы',
  month_few: 'месяца',
  month_many: 'месяцев',
  Bars: 'Бары',
  Line: 'Линия',
  Baseline: 'Базовая линия',
  Candles: 'Свечи',
  'Time format': 'Формат времени',
  'Date format': 'Формат даты',
  'Line start': 'Начало линии',
  'Line end': 'Конец линии',
  Normal: 'Обычный',
  Crosshair: 'Перекрестие',
  Arrow: 'Стрелка',
  'Trend line': 'Линия тренда',
  'Trend lines': 'Линии тренда',
  Ray: 'Луч',
  'Horizontal line': 'Горизонтальная линия',
  'Horizontal ray': 'Горизонтальный луч',
  'Vertical line': 'Вертикальная линия',
  'Vertical ray': 'Вертикальный луч',
  'Long position': 'Длинная позиция',
  'Short position': 'Короткая позиция',
  'Dates range': 'Диапазон дат',
  'Prices range': 'Диапазон цен',
  'Fixed range volume profile': 'Профиль объема в заданном диапазоне',
  'Anchored volume profile': 'Профиль объема с привязкой',
  'Fibonacci retracement': 'Коррекция Фибоначчи',
  Rectangle: 'Прямоугольник',
  Traectory: 'Траектория',
  'Geometric shapes': 'Геометрические фигуры',
  Text: 'Текст',
  'XABCD template': 'Шаблон XABCD',
  'Measurement tools': 'Инструменты для измерения и прогнозирования',
  Ruler: 'Линейка',
  Upscale: 'Увеличить масштаб',
  Downscale: 'Уменьшить масштаб',
  'Gann and Fibonacci': 'Инструменты Ганна и Фибоначчи',
  'Endless drawing mode': 'Продолжить в режиме рисования',
  'Fix all objects': 'Зафиксировать все объекты',
  'Hide all drawing objects': 'Скрыть все объекты рисования',
  'Clear objects': 'Удалить объекты',
  'DOM tree': 'Дерево объектов',
  'Magnet allows you to attract objects points to the nearest bar prices':
    'Магнит позволяет притягивать точки объектов к ближайшим ценам (откр., макс., мин., закр.) баров',
  Pin: 'Значок',
  Open: 'Откр.',
  High: 'Макс.',
  Low: 'Мин.',
  Close: 'Закр.',
  Change: 'Изм.',
  Time: 'Время',
  Arguments: 'Аргументы',

  'Account size': 'Размер счёта',
  'Lot size': 'Размер лота',
  Risk: 'Риск',
  'Entry price': 'Цена открытия',
  Leverage: 'Кредитное плечо',
  'Profit price': 'Цена цели',
  'Stop price': 'Цена стоп-уровня',
  'QTY precision': 'Точность количества',
  'Line width': 'Толщина линии',
  'Stop color': 'Цвет стоп-уровня',
  'Target color': 'Цвет цели',

  // settings
  Area: 'Область',
  Border: 'Граница',
  'Font size': 'Размер текста',
  Bold: 'Жирный',
  Italic: 'Курсив',
  'Text color': 'Цвет текста',
  'Background color': 'Цвет фона',
  Lock: 'Заблокировать',
  Unlock: 'Разблокировать',
  Remove: 'Удалить',
  Style: 'Стиль',
  'Background transparency': 'Прозрачность фона',
  Background: 'Фон',
  Opacity: 'Прозрачность',
  Reverse: 'Переворот',
  'Labels position': 'Положение меток',
  'Show prices': 'Отображать цену',
  'Show level values': 'Отображать уровень',
  Color: 'Цвет',
  Levels: 'Уровни',
  Level: 'Уровень',
  Left: 'Слева',
  Right: 'Справа',
  'Enter text': 'Введите текст',
  'Profit zone': 'Зона прибыли',
  'Loss zone': 'Зона убытка',
  'Line color': 'Цвет линии',
  'Growing volume': 'Растущий объём',
  'Decreasing volume': 'Снижающий объём',
  'POC-line': 'POC-линия',
  'Parallel channel': 'Параллельный канал',
  'Regression trend': 'Регрессионный тренд',
  'Middle line': 'Средняя линия',
  'Channel line': 'Граница канала',
  'Upper area': 'Верхняя зона',
  'Lower area': 'Нижняя зона',
  'Show middle line': 'Показывать среднюю линию',
  'Show correlation': 'Показывать корреляцию',
  Length: 'Длинна',
  Data: 'Данные',
  'Open price': 'Цена открытия',
  Max: 'Максимум',
  Min: 'Минимум',
  'Close price': 'Цена закрытия',
  Offset: 'Отступ',
  'Fast length': 'Длина Fast',
  'Slow length': 'Длина Slow',
  'Signal length': 'Длина сигнала',
  'Oscillator MA type': 'Тип осцилятора MA',
  'Signal MA type': 'Тип сигнала MA',
  'MA length': 'Длина MA',
  Save: 'Сохранить',
  Reject: 'Отменить',
  Volume: 'Объём',
  '1D': '1Д',
  '5D': '5Д',
  '1M': '1М',
  '3M': '3М',
  '6M': '6М',
  '1Y': '1Г',
  All: 'Всё',
  '12h': '12ч',
  '24h': '24ч',
  "Mon Q3 '97": "Пн кв3 '97",
  'Mon Q3 1997': 'Пн кв3 1997',
  "Mon 29 Sep '97": "Пн 29 сен. '97",
  "Mon Sep '97": "Пн сен. '97",
  'Mon Sep 29, 1997': 'Пн сен. 29, 1997',
  'Mon Sep 1997': 'Пн сен. 1997',
  'Mon Sep 29': 'Пн сен. 29',
  'Mon 29 Sep': 'Пн 29 сен.',
  'Mon 1997-09-29': 'Пн 1997-09-29',
  'Mon 97-09-29': 'Пн 97-09-29',
  'Mon 97/09/29': 'Пн 97/09/29',
  'Mon 1997/09/29': 'Пн 1997/09/29',
  'Mon 29-09-1997': 'Пн 29-09-1997',
  'Mon 29-09-97': 'Пн 29-09-97',
  'Mon 29/09/97': 'Пн 29/09/97',
  'Mon 29/09/1997': 'Пн 29/09/1997',
  'Mon 09/29/97': 'Пн 09/29/97',
  'Mon 09/29/1997': 'Пн 09/29/1997',
  '09.29.1997 00:00:00': '09.29.1997 00:00:00',
  'interval for': 'интервал для',
  timeframe: 'таймфрейма',
  'Open P&L': 'Откр. поз. П/Л',
  Qty: 'Кол-во',
  'Risk/Reward Ratio': 'Соотношение риск/прибыль',
  Target: 'Цель',
  Amount: 'Количество',
  Stop: 'Стоп',
  Vol: 'Объем',
  bars: 'бары',
  AutoScale: 'Автомасштаб',
  Logarithmic: 'Логарифмическая',

  '1t': '1т',
  '10t': '10т',
  '100t': '100т',
  '1000t': '1000т',

  '1s': '1с',
  '5s': '5с',
  '10s': '10с',
  '15s': '15с',
  '30s': '30с',
  '45s': '45с',

  '1m': '1м',
  '2m': '2м',
  '3m': '3м',
  '5m': '5м',
  '10m': '10м',
  '15m': '15м',
  '30m': '30м',
  '45m': '45м',

  '1h': '1ч',
  '2h': '2ч',
  '3h': '3ч',
  '4h': '4ч',
  '8h': '8ч',

  '1d': '1д',
  '3d': '3д',
  '5d': '5д',

  '1w': '1н',

  '1М': '1M',
  '3М': '3M',

  'Add a main instrument to display indicators': 'Добавьте основной инструмент для отображения индикаторов',
  'Show drawing toolbar': 'Показать панель рисования',
  'Hide drawing toolbar': 'Скрыть панель рисования',
  'Symbol search': 'Поиск инструмента',
  'Compare symbols': 'Сравнить инструменты',
  Timeframe: 'Таймфрейм',
  'Chart type': 'Тип графика',
  Undo: 'Отменить',
  Redo: 'Повторить',
  Snapshot: 'Снимок графика',
  'Copy image': 'Копировать изображение',
  Fullscreen: 'Во весь экран',
  'Exit fullscreen': 'Выйти из полноэкранного режима',

  Price: 'Цена',
  Percent: 'Процент',
  'Loading seasonality': 'Загрузка сезонности…',
  'No seasonality data': 'Нет данных за выбранные годы',
  'Unable to load seasonality': 'Не удалось загрузить сезонность',
  'Percentage requires a positive baseline': 'Для процентов нужна положительная базовая цена года',
  Chart: 'График',
  Seasonality: 'Сезонность',
  Average: 'Среднее',
  Avg: 'Средн.',
  Date: 'Дата',
  Year: 'Год',
  January: 'Январь',
  February: 'Февраль',
  March: 'Март',
  April: 'Апрель',
  May: 'Май',
  June: 'Июнь',
  July: 'Июль',
  August: 'Август',
  September: 'Сентябрь',
  October: 'Октябрь',
  November: 'Ноябрь',
  December: 'Декабрь',
};