diff --git a/app/Http/Controllers/LocaleController.php b/app/Http/Controllers/LocaleController.php new file mode 100644 index 0000000..28dccb3 --- /dev/null +++ b/app/Http/Controllers/LocaleController.php @@ -0,0 +1,20 @@ +session()->put('locale', $locale); + + return back(); + } +} diff --git a/app/Http/Controllers/Shop/AccountController.php b/app/Http/Controllers/Shop/AccountController.php index ba04c8d..17f91f2 100644 --- a/app/Http/Controllers/Shop/AccountController.php +++ b/app/Http/Controllers/Shop/AccountController.php @@ -28,6 +28,6 @@ class AccountController extends Controller $request->user()->update($validated); - return back()->with('status', 'Данные профиля обновлены.'); + return back()->with('status', __('Данные профиля обновлены.')); } } diff --git a/app/Http/Controllers/Shop/AuthController.php b/app/Http/Controllers/Shop/AuthController.php index bacba8c..99d6bc2 100644 --- a/app/Http/Controllers/Shop/AuthController.php +++ b/app/Http/Controllers/Shop/AuthController.php @@ -34,7 +34,7 @@ class AuthController extends Controller if (!$this->captchaIsValid($request, self::LOGIN_CAPTCHA_CONTEXT)) { return back() ->withInput($request->only('email', 'remember')) - ->withErrors(['captcha' => 'Неверный ответ на капчу.']); + ->withErrors(['captcha' => __('Неверный ответ на капчу.')]); } $credentials = [ @@ -47,7 +47,7 @@ class AuthController extends Controller if (!Auth::attempt($credentials, $remember)) { return back() ->withInput($request->only('email', 'remember')) - ->withErrors(['email' => 'Неверный email или пароль.']); + ->withErrors(['email' => __('Неверный email или пароль.')]); } $request->session()->regenerate(); @@ -75,7 +75,7 @@ class AuthController extends Controller if (!$this->captchaIsValid($request, self::REGISTER_CAPTCHA_CONTEXT)) { return back() ->withInput($request->only('name', 'email')) - ->withErrors(['captcha' => 'Неверный ответ на капчу.']); + ->withErrors(['captcha' => __('Неверный ответ на капчу.')]); } $user = User::create([ diff --git a/app/Http/Controllers/Shop/CartController.php b/app/Http/Controllers/Shop/CartController.php index 75f2de8..88eaecf 100644 --- a/app/Http/Controllers/Shop/CartController.php +++ b/app/Http/Controllers/Shop/CartController.php @@ -46,20 +46,20 @@ class CartController extends Controller public function add(Product $product) { if (!$product->is_active || $product->stock < 1) { - return back()->with('status', 'Товар сейчас недоступен для заказа.'); + return back()->with('status', __('Товар сейчас недоступен для заказа.')); } $cart = (array) session()->get('cart', []); $current = (int) ($cart[$product->id] ?? 0); if ($current >= $product->stock) { - return back()->with('status', 'В корзине уже максимальное доступное количество.'); + return back()->with('status', __('В корзине уже максимальное доступное количество.')); } $cart[$product->id] = $current + 1; session()->put('cart', $cart); - return back()->with('status', "Товар \"{$product->name}\" добавлен в корзину."); + return back()->with('status', __('Товар ":name" добавлен в корзину.', ['name' => $product->name])); } public function update(Request $request, Product $product) @@ -80,15 +80,15 @@ class CartController extends Controller unset($cart[$product->id]); session()->put('cart', $cart); - return back()->with('status', "Товар \"{$product->name}\" удален из корзины."); + return back()->with('status', __('Товар ":name" удален из корзины.', ['name' => $product->name])); } $cart[$product->id] = $quantity; session()->put('cart', $cart); $message = $quantity < (int) $validated['quantity'] - ? 'Количество ограничено текущим остатком.' - : 'Количество товара обновлено.'; + ? __('Количество ограничено текущим остатком.') + : __('Количество товара обновлено.'); return back()->with('status', $message); } @@ -101,6 +101,6 @@ class CartController extends Controller session()->put('cart', $cart); } - return back()->with('status', "Товар \"{$product->name}\" удален из корзины."); + return back()->with('status', __('Товар ":name" удален из корзины.', ['name' => $product->name])); } } diff --git a/app/Http/Controllers/Shop/ChatController.php b/app/Http/Controllers/Shop/ChatController.php index 706a5a6..b478719 100644 --- a/app/Http/Controllers/Shop/ChatController.php +++ b/app/Http/Controllers/Shop/ChatController.php @@ -40,7 +40,7 @@ class ChatController extends Controller $messageText = $this->sanitizeMessage((string) $validated['message']); if ($messageText === '') { throw ValidationException::withMessages([ - 'message' => 'Сообщение содержит недопустимые символы.', + 'message' => __('Сообщение содержит недопустимые символы.'), ]); } @@ -156,7 +156,7 @@ class ChatController extends Controller 'status' => $conversation->status, 'is_closed' => $conversation->isClosed(), 'notice' => $conversation->isClosed() - ? 'Чат закрыт администратором. Отправьте новое сообщение, чтобы начать новый диалог.' + ? __('Чат закрыт администратором. Отправьте новое сообщение, чтобы начать новый диалог.') : null, ]; } diff --git a/app/Http/Controllers/Shop/CheckoutController.php b/app/Http/Controllers/Shop/CheckoutController.php index eb4e730..9402902 100644 --- a/app/Http/Controllers/Shop/CheckoutController.php +++ b/app/Http/Controllers/Shop/CheckoutController.php @@ -19,7 +19,7 @@ class CheckoutController extends Controller $items = $this->cartItems($request); if ($items->isEmpty()) { - return redirect()->route('cart.index')->with('status', 'Корзина пустая. Добавьте товары перед оформлением.'); + return redirect()->route('cart.index')->with('status', __('Корзина пустая. Добавьте товары перед оформлением.')); } return view('shop.checkout', [ @@ -34,7 +34,7 @@ class CheckoutController extends Controller $items = $this->cartItems($request); if ($items->isEmpty()) { - return redirect()->route('cart.index')->with('status', 'Корзина пустая. Добавьте товары перед оформлением.'); + return redirect()->route('cart.index')->with('status', __('Корзина пустая. Добавьте товары перед оформлением.')); } $validated = $request->validate([ @@ -56,12 +56,12 @@ class CheckoutController extends Controller $items = $this->cartItems($request); if ($items->isEmpty()) { - return redirect()->route('cart.index')->with('status', 'Корзина пустая. Добавьте товары перед оформлением.'); + return redirect()->route('cart.index')->with('status', __('Корзина пустая. Добавьте товары перед оформлением.')); } $customer = $request->session()->get(self::CHECKOUT_CUSTOMER_KEY); if (!is_array($customer)) { - return redirect()->route('checkout.show')->with('status', 'Сначала заполните данные получателя.'); + return redirect()->route('checkout.show')->with('status', __('Сначала заполните данные получателя.')); } return view('shop.checkout-payment', [ @@ -77,12 +77,12 @@ class CheckoutController extends Controller $items = $this->cartItems($request); if ($items->isEmpty()) { - return redirect()->route('cart.index')->with('status', 'Корзина пустая. Добавьте товары перед оформлением.'); + return redirect()->route('cart.index')->with('status', __('Корзина пустая. Добавьте товары перед оформлением.')); } $validated = $request->session()->get(self::CHECKOUT_CUSTOMER_KEY); if (!is_array($validated)) { - return redirect()->route('checkout.show')->with('status', 'Сначала заполните данные получателя.'); + return redirect()->route('checkout.show')->with('status', __('Сначала заполните данные получателя.')); } $validator = validator($validated, [ diff --git a/app/Http/Controllers/Shop/CompareController.php b/app/Http/Controllers/Shop/CompareController.php index 206ce33..d506bbf 100644 --- a/app/Http/Controllers/Shop/CompareController.php +++ b/app/Http/Controllers/Shop/CompareController.php @@ -44,24 +44,24 @@ class CompareController extends Controller $compare = array_values(array_filter($compare, fn (int $id) => $id !== $product->id)); session()->put('compare', $compare); - return back()->with('status', "Товар \"{$product->name}\" удален из сравнения."); + return back()->with('status', __('Товар ":name" удален из сравнения.', ['name' => $product->name])); } if (count($compare) >= 4) { - return back()->with('status', 'Можно сравнить не более 4 товаров одновременно.'); + return back()->with('status', __('Можно сравнить не более 4 товаров одновременно.')); } $compare[] = $product->id; session()->put('compare', array_values(array_unique($compare))); - return back()->with('status', "Товар \"{$product->name}\" добавлен в сравнение."); + return back()->with('status', __('Товар ":name" добавлен в сравнение.', ['name' => $product->name])); } public function clear() { session()->forget('compare'); - return back()->with('status', 'Список сравнения очищен.'); + return back()->with('status', __('Список сравнения очищен.')); } private function compareIds(): array diff --git a/app/Http/Controllers/Shop/ContactController.php b/app/Http/Controllers/Shop/ContactController.php index 36a6a63..c1f9490 100644 --- a/app/Http/Controllers/Shop/ContactController.php +++ b/app/Http/Controllers/Shop/ContactController.php @@ -24,7 +24,7 @@ class ContactController extends Controller if ($botToken === '' || $chatId === '') { return back() ->withInput() - ->withErrors(['contact' => 'Не настроена отправка в Telegram. Заполните SHOP_TELEGRAM_BOT_TOKEN и SHOP_TELEGRAM_CHAT_ID.']); + ->withErrors(['contact' => __('Не настроена отправка в Telegram. Заполните SHOP_TELEGRAM_BOT_TOKEN и SHOP_TELEGRAM_CHAT_ID.')]); } $message = $this->buildTelegramMessage($validated, $request); @@ -41,16 +41,16 @@ class ContactController extends Controller } catch (Throwable) { return back() ->withInput() - ->withErrors(['contact' => 'Не удалось отправить заявку в Telegram. Попробуйте еще раз.']); + ->withErrors(['contact' => __('Не удалось отправить заявку в Telegram. Попробуйте еще раз.')]); } if (!$response->successful() || $response->json('ok') !== true) { return back() ->withInput() - ->withErrors(['contact' => 'Telegram не принял заявку. Проверьте токен бота и chat id.']); + ->withErrors(['contact' => __('Telegram не принял заявку. Проверьте токен бота и chat id.')]); } - return back()->with('status', 'Заявка отправлена. Мы свяжемся с вами в ближайшее время.'); + return back()->with('status', __('Заявка отправлена. Мы свяжемся с вами в ближайшее время.')); } private function buildTelegramMessage(array $data, Request $request): string diff --git a/app/Http/Controllers/Shop/FavoriteController.php b/app/Http/Controllers/Shop/FavoriteController.php index 34f432c..45573e7 100644 --- a/app/Http/Controllers/Shop/FavoriteController.php +++ b/app/Http/Controllers/Shop/FavoriteController.php @@ -34,13 +34,13 @@ class FavoriteController extends Controller $favorites = array_values(array_filter($favorites, fn (int $id) => $id !== $product->id)); session()->put('favorites', $favorites); - return back()->with('status', "Товар \"{$product->name}\" удален из избранного."); + return back()->with('status', __('Товар ":name" удален из избранного.', ['name' => $product->name])); } $favorites[] = $product->id; session()->put('favorites', array_values(array_unique($favorites))); - return back()->with('status', "Товар \"{$product->name}\" добавлен в избранное."); + return back()->with('status', __('Товар ":name" добавлен в избранное.', ['name' => $product->name])); } private function favoriteIds(): array diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php new file mode 100644 index 0000000..724def1 --- /dev/null +++ b/app/Http/Middleware/SetLocale.php @@ -0,0 +1,25 @@ +session()->get('locale', $defaultLocale); + if (!in_array($locale, $supportedLocales, true)) { + $locale = $defaultLocale; + } + + app()->setLocale($locale); + + return $next($request); + } +} diff --git a/app/Models/ChatConversation.php b/app/Models/ChatConversation.php index 5595076..406bba6 100644 --- a/app/Models/ChatConversation.php +++ b/app/Models/ChatConversation.php @@ -47,7 +47,7 @@ class ChatConversation extends Model return $this->user->name; } - return 'Гость #' . $this->id; + return __('Гость #:number', ['number' => $this->id]); } public function isClosed(): bool diff --git a/app/Models/Order.php b/app/Models/Order.php index 5032dca..7386890 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -43,8 +43,21 @@ class Order extends Model public function getPaymentMethodLabelAttribute(): string { return match ($this->payment_method) { - 'card_transfer' => 'Перевод по реквизитам (на карту)', - default => 'Не указан', + 'card_transfer' => __('Перевод по реквизитам (на карту)'), + default => __('Не указан'), + }; + } + + public function getStatusLabelAttribute(): string + { + return match ($this->status) { + 'new' => __('Новый'), + 'processing' => __('В обработке'), + 'paid' => __('Оплачен'), + 'shipped' => __('Отправлен'), + 'completed' => __('Завершен'), + 'cancelled' => __('Отменен'), + default => (string) $this->status, }; } } diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..673c147 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ withMiddleware(function (Middleware $middleware): void { - // + $middleware->web(append: [ + SetLocale::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/config/app.php b/config/app.php index 423eed5..587fb71 100644 --- a/config/app.php +++ b/config/app.php @@ -78,11 +78,30 @@ return [ | */ - 'locale' => env('APP_LOCALE', 'en'), + 'locale' => env('APP_LOCALE', 'ru'), - 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'ru'), - 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + 'faker_locale' => env('APP_FAKER_LOCALE', 'ru_RU'), + + 'supported_locales' => [ + 'ru' => [ + 'label' => 'Русский', + 'native' => 'Русский', + 'short' => 'Рус', + 'flag' => '🇷🇺', + 'hreflang' => 'ru-RU', + 'og_locale' => 'ru_RU', + ], + 'kk' => [ + 'label' => 'Kazakh', + 'native' => 'Қазақша', + 'short' => 'Қаз', + 'flag' => '🇰🇿', + 'hreflang' => 'kk-KZ', + 'og_locale' => 'kk_KZ', + ], + ], /* |-------------------------------------------------------------------------- diff --git a/deploy/nginx/pc-shop.conf b/deploy/nginx/pc-shop.conf index e2abad2..c5cc046 100644 --- a/deploy/nginx/pc-shop.conf +++ b/deploy/nginx/pc-shop.conf @@ -1,7 +1,7 @@ server { listen 80; listen [::]:80; - server_name shop.example.com www.shop.example.com; + server_name tehnobox.shop www.tehnobox.shop; root /var/www/pc-shop/public; location ^~ /.well-known/acme-challenge/ { @@ -17,12 +17,12 @@ server { server { listen 443 ssl http2; listen [::]:443 ssl http2; - server_name shop.example.com www.shop.example.com; + server_name tehnobox.shop www.tehnobox.shop; root /var/www/pc-shop/public; index index.php; - ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/tehnobox.shop/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/tehnobox.shop/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; diff --git a/lang/kk.json b/lang/kk.json new file mode 100644 index 0000000..68474d8 --- /dev/null +++ b/lang/kk.json @@ -0,0 +1,402 @@ +{ + "(c) :year :company. Все права защищены.": "(c) :year :company. Барлық құқықтар қорғалған.", + "1-3 дня по стране": "Ел бойынша 1-3 күн", + "8 ядер, 16 потоков, до 5.4 ГГц.": "8 ядро, 16 ағын, 5.4 ГГц-ке дейін.", + "12 ГБ GDDR6X, DLSS 3.": "12 ГБ GDDR6X, DLSS 3.", + "13.5\\\", компактный ультрабук.": "13.5\\\", ықшам ультрабук.", + "13.6\\\", чип M2, 8/256 ГБ.": "13.6\\\", M2 чипі, 8/256 ГБ.", + "14\\\", легкий для работы.": "14\\\", жұмысқа жеңіл.", + "15.6\\\", Ryzen 7, RTX 4060.": "15.6\\\", Ryzen 7, RTX 4060.", + "16 ГБ GDDR6, отличная производительность.": "16 ГБ GDDR6, жоғары өнімділік.", + "16 ядер, для мощных систем.": "16 ядро, қуатты жүйелерге арналған.", + "43\\\", 4K UHD, Android TV.": "43\\\", 4K UHD, Android TV.", + "50\\\", NanoCell, webOS.": "50\\\", NanoCell, webOS.", + "55\\\", 4K UHD, Smart TV.": "55\\\", 4K UHD, Smart TV.", + "6.1\\\", 128 ГБ, отличное состояние.": "6.1\\\", 128 ГБ, өте жақсы күйде.", + "650 Вт, стабильная линия питания.": "650 Вт, тұрақты қорек желісі.", + "750 Вт, 80 Plus Gold.": "750 Вт, 80 Plus Gold.", + "850 Вт, тихий режим работы.": "850 Вт, тыныш жұмыс режимі.", + ":category, комплектующие, купить, фильтры товаров": ":category, жинақтаушы, сатып алу, тауар сүзгілері", + ":company работает для тех, кому важно собрать быстрый и надежный ПК без ошибок по совместимости. В каталоге есть комплектующие для домашних, рабочих и игровых систем: процессоры, материнские платы, видеокарты, память, накопители, блоки питания, корпуса, системы охлаждения, ноутбуки и периферия.": ":company үйлесімділік қателерінсіз жылдам әрі сенімді ПК жинау маңызды адамдар үшін жұмыс істейді. Каталогта үйге, жұмысқа және ойынға арналған жүйелерге керек бөлшектер бар: процессорлар, аналық платалар, бейнекарталар, жад, жинақтауыштар, қуат блоктары, корпустар, салқындату жүйелері, ноутбуктер және периферия.", + ":company — интернет-магазин компьютерных комплектующих.": ":company — компьютер бөлшектері интернет-дүкені.", + ":company — поддержка клиентов": ":company — клиенттерге қолдау", + ":label: :from - :to": ":label: :from - :to", + ":product - миниатюра :number": ":product - миниатюра :number", + ":product - фото :number": ":product - сурет :number", + ":product, :category, купить": ":product, :category, сатып алу", + "AM5, усиленное питание, PCIe 4.0.": "AM5, күшейтілген қорек, PCIe 4.0.", + "ATX, сетчатый фронт.": "ATX, торлы алдыңғы панель.", + "Apple": "Apple", + "Smart TV": "Smart TV", + "Telegram не принял заявку. Проверьте токен бота и chat id.": "Telegram өтінімді қабылдамады. Бот токені мен chat id тексеріңіз.", + "Telegram": "Telegram", + "DDR4 3200 МГц, 2x8 ГБ.": "DDR4 3200 МГц, 2x8 ГБ.", + "DDR4 3600 МГц, 2x16 ГБ.": "DDR4 3600 МГц, 2x16 ГБ.", + "DDR5 6000 МГц, 2x16 ГБ.": "DDR5 6000 МГц, 2x16 ГБ.", + "GPU": "GPU", + "Email": "Email", + "NVMe SSD с высокой скоростью.": "Жоғары жылдамдықты NVMe SSD.", + "SSD и HDD для хранения данных.": "Деректерді сақтауға арналған SSD және HDD.", + "AM5": "AM5", + "Android TV": "Android TV", + "DLSS 3.": "DLSS 3.", + "mATX": "mATX", + "Wi‑Fi": "Wi‑Fi", + "Wi-Fi": "Wi-Fi", + "Адрес доставки": "Жеткізу мекенжайы", + "Артикул:": "Артикул:", + "Банк": "Банк", + "Банковский перевод": "Банктік аударым", + "Безналичный расчет для юрлиц": "Заңды тұлғаларға қолма-қолсыз есеп айырысу", + "Белый": "Ақ", + "Блоки питания": "Қуат блоктары", + "Блоки питания.": "Қуат блоктары.", + "Быстрая доставка и удобная оплата.": "Жылдам жеткізу және ыңғайлы төлем.", + "Б/у": "Қолданылған", + "В избранное": "Таңдаулыларға", + "В корзине": "Себетте", + "В корзине уже максимальное доступное количество.": "Себетте қолжетімді ең көп саны бар.", + "В корзину": "Себетке", + "В обработке": "Өңделуде", + "В сравнение": "Салыстыруға", + "Варианты доставки": "Жеткізу нұсқалары", + "Ваш заказ": "Сіздің тапсырысыңыз", + "Ваше имя": "Атыңыз", + "Введите email и пароль для доступа к заказам и профилю.": "Тапсырыстар мен профильге кіру үшін email мен құпиясөзді енгізіңіз.", + "Введите запрос в строку поиска, чтобы открыть список найденных товаров.": "Табылған тауарлар тізімін ашу үшін іздеу жолына сұраныс енгізіңіз.", + "Введите название товара, чтобы увидеть найденные позиции.": "Табылған тауарларды көру үшін атауын енгізіңіз.", + "Введите сообщение...": "Хабарлама енгізіңіз...", + "Видеокарты": "Бейнекарталар", + "Войти": "Кіру", + "Вперед": "Алға", + "Все": "Барлығы", + "Все категории": "Барлық санаттар", + "Вход": "Кіру", + "Выбирайте курьера или доставку по времени с безопасной оплатой.": "Курьерді не уақыт бойынша жеткізуді таңдап, қауіпсіз төлем жасаңыз.", + "Выбор языка": "Тілді таңдау", + "Выйти": "Шығу", + "Главная": "Басты бет", + "Гость #:number": "Қонақ №:number", + "Данные аккаунта": "Аккаунт деректері", + "Данные получателя": "Алушы деректері", + "Данные профиля обновлены.": "Профиль деректері жаңартылды.", + "День в день в пределах города": "Қала ішінде сол күні", + "Для нас важны прозрачность и сервис: актуальные цены, понятные характеристики и честная обратная связь. Мы стремимся, чтобы покупка техники была удобной как для новичков, так и для опытных пользователей, которые собирают ПК самостоятельно.": "Біз үшін ашықтық пен сервис маңызды: өзекті бағалар, түсінікті сипаттамалар және адал кері байланыс. Жаңадан бастағандарға да, компьютерді өздері жинайтын тәжірибелі қолданушыларға да техника сатып алу ыңғайлы болғанын қалаймыз.", + "Для этой категории фильтры пока не заданы.": "Бұл санат үшін сүзгілер әлі берілмеген.", + "До": "Дейін", + "Добавить в избранное": "Таңдаулыларға қосу", + "Добавить в сравнение": "Салыстыруға қосу", + "Добавьте товары в избранное из каталога или со страницы товара.": "Тауарларды каталогтан немесе тауар бетінен таңдаулыларға қосыңыз.", + "Добавьте товары в сравнение из карточек каталога.": "Тауарларды каталог карточкаларынан салыстыруға қосыңыз.", + "Добавьте товары из каталога, чтобы оформить заказ.": "Тапсырысты рәсімдеу үшін каталогтан тауар қосыңыз.", + "Дополнительные изображения товара": "Тауардың қосымша суреттері", + "Доставка": "Жеткізу", + "Доставка и оплата": "Жеткізу және төлем", + "Доставка курьером по городу": "Қала бойынша курьермен жеткізу", + "Если вам нужна консультация перед покупкой, команда :company поможет подобрать комплектующие и предложит сбалансированные варианты под ваши задачи.": "Сатып алар алдында кеңес керек болса, :company командасы қажетті бөлшектерді таңдап, міндеттеріңізге сай теңгерімді нұсқаларды ұсынады.", + "Жесткие диски": "Қатты дискілер", + "Жидкостное охлаждение 240 мм.": "240 мм сұйық салқындату.", + "Завершен": "Аяқталды", + "Задайте вопрос — администратор ответит в этом окне.": "Сұрақ қойыңыз — әкімші осы терезеде жауап береді.", + "Заказ #:number": "Тапсырыс №:number", + "Заказ оформлен": "Тапсырыс рәсімделді", + "Заказ №:number успешно оформлен": "Тапсырыс №:number сәтті рәсімделді", + "Закрыть меню": "Мәзірді жабу", + "Заполните контакты и перейдите на страницу с реквизитами для оплаты.": "Байланыс деректерін толтырып, төлем деректемелері бетіне өтіңіз.", + "Запомнить меня": "Мені есте сақтау", + "Запрос: \":query\"": "Сұраныс: \":query\"", + "Зарегистрироваться": "Тіркелу", + "Заявка отправлена. Мы свяжемся с вами в ближайшее время.": "Өтінім жіберілді. Жақын арада хабарласамыз.", + "Золотой": "Алтын түсті", + "Избранное": "Таңдаулылар", + "Избранные товары": "Таңдаулы тауарлар", + "Изменить данные": "Деректерді өзгерту", + "Имя": "Аты", + "Имя получателя": "Алушының аты", + "Интернет-магазин компьютерных комплектующих": "Компьютер бөлшектері интернет-дүкені", + "Интернет-магазин компьютерных комплектующих, ноутбуков и периферии. Подбор, сравнение и заказ в одном месте.": "Компьютер бөлшектері, ноутбуктер және периферия интернет-дүкені. Таңдау, салыстыру және тапсырыс — бәрі бір жерде.", + "Интернет-магазин комплектующих для ПК: процессоры, материнские платы, видеокарты, ноутбуки и периферия.": "ПК бөлшектерінің интернет-дүкені: процессорлар, аналық платалар, бейнекарталар, ноутбуктер және периферия.", + "Искать": "Іздеу", + "Итого": "Барлығы", + "Кабинет": "Кабинет", + "Как мы помогаем клиентам": "Клиенттерге қалай көмектесеміз", + "Капча: решите пример :question": "Капча: :question мысалын шешіңіз", + "Каталог": "Каталог", + "Каталог компьютерных комплектующих и техники.": "Компьютер бөлшектері мен техниканың каталогы.", + "Каталог компьютерных комплектующих: процессоры, материнские платы, видеокарты, память, накопители и ноутбуки.": "Компьютер бөлшектерінің каталогы: процессорлар, аналық платалар, бейнекарталар, жад, жинақтауыштар және ноутбуктер.", + "Каталог товаров": "Тауарлар каталогы", + "Категории": "Санаттар", + "Категории пока не добавлены.": "Санаттар әлі қосылмаған.", + "Категории товаров": "Тауар санаттары", + "Категория": "Санат", + "Категория товаров :category": ":category санаты", + "Количество вентиляторов": "Желдеткіш саны", + "Количество модулей": "Модуль саны", + "Количество ограничено текущим остатком.": "Саны ағымдағы қалдықпен шектелді.", + "Количество товара обновлено.": "Тауар саны жаңартылды.", + "Количество ядер": "Ядро саны", + "Комментарий к заказу": "Тапсырысқа түсініктеме", + "Комментарий:": "Түсініктеме:", + "Компания": "Компания", + "Компактная mATX для LGA1700.": "LGA1700 үшін ықшам mATX.", + "Компактный и продуваемый.": "Ықшам әрі жақсы желдетіледі.", + "Контакты": "Байланыс", + "Контакты магазина: телефон, email, адрес и часы работы поддержки.": "Дүкен байланыстары: телефон, email, мекенжай және қолдау уақыты.", + "Корзина": "Себет", + "Корзина пустая": "Себет бос", + "Корзина пустая. Добавьте товары перед оформлением.": "Себет бос. Рәсімдеу алдында тауар қосыңыз.", + "Корпуса": "Корпустар", + "Корпуса с отличным airflow.": "Ауа айналымы жақсы корпустар.", + "Кто мы": "Біз кімбіз", + "Купить :product по выгодной цене.": ":product-ті тиімді бағамен сатып алыңыз.", + "Кэш көлемі": "Кэш көлемі", + "Личный кабинет": "Жеке кабинет", + "Макс. длина видеокарты": "Бейнекартаның ең ұзын өлшемі", + "Максимальный объем памяти": "Жадтың ең көп көлемі", + "Максимальный объем памяти (ГБ)": "Жадтың ең көп көлемі (ГБ)", + "Материнские платы": "Аналық платалар", + "Меню": "Мәзір", + "Мини‑корпус для ITX.": "ITX үшін шағын корпус.", + "Модель": "Модель", + "Модель видеокарты": "Бейнекарта моделі", + "Модель процессора": "Процессор моделі", + "Можно сравнить не более 4 товаров одновременно.": "Бір уақытта 4 тауардан артық салыстыруға болмайды.", + "Мои заказы": "Менің тапсырыстарым", + "Мощность": "Қуаты", + "Мы делаем акцент на понятном выборе: категории с фильтрами, сравнение товаров, избранное, корзина и личный кабинет с историей заказов. Это помогает быстрее принять решение и не потерять важные позиции при подборе сборки.": "Біз түсінікті таңдауға мән береміз: сүзгілері бар санаттар, тауарларды салыстыру, таңдаулылар, себет және тапсырыс тарихы бар жеке кабинет. Бұл шешімді тезірек қабылдауға және маңызды позицияларды жоғалтпауға көмектеседі.", + "Мы помогаем подобрать совместимую сборку, оформить заказ и получить технику с понятной поддержкой после покупки.": "Біз үйлесімді жинақ таңдауға, тапсырыс рәсімдеуге және сатып алғаннан кейін түсінікті қолдаумен техниканы алуға көмектесеміз.", + "Мы приняли заказ в обработку. Статус заказа:": "Тапсырысыңызды өңдеуге қабылдадық. Тапсырыс мәртебесі:", + "Назад": "Артқа", + "Назначение платежа:": "Төлем мақсаты:", + "Найденные товары по запросу «:query». Выберите подходящий товар и откройте подробную карточку.": "«:query» сұранысы бойынша табылған тауарлар. Сәйкес тауарды таңдап, толық карточканы ашыңыз.", + "Найдено товаров:": "Табылған тауар саны:", + "Найдено:": "Табылды:", + "Найти": "Іздеу", + "Например, Ryzen 7 или RTX 4060": "Мысалы, Ryzen 7 немесе RTX 4060", + "Начать новый чат": "Жаңа чатты бастау", + "Наш подход": "Біздің ұстаным", + "Не настроена отправка в Telegram. Заполните SHOP_TELEGRAM_BOT_TOKEN и SHOP_TELEGRAM_CHAT_ID.": "Telegram-ға жіберу бапталмаған. SHOP_TELEGRAM_BOT_TOKEN және SHOP_TELEGRAM_CHAT_ID толтырыңыз.", + "Не удалось отправить заявку в Telegram. Попробуйте еще раз.": "Telegram-ға өтінімді жіберу мүмкін болмады. Қайта көріңіз.", + "Не указан": "Көрсетілмеген", + "Неверный email или пароль.": "Email немесе құпиясөз қате.", + "Неверный ответ на капчу.": "Капчаға жауап қате.", + "Надежные блоки питания.": "Сенімді қуат блоктары.", + "Надежные платы для любых конфигураций.": "Кез келген конфигурацияға арналған сенімді платалар.", + "Надежный HDD для хранения.": "Сақтауға арналған сенімді HDD.", + "Нет в наличии": "Қоймада жоқ", + "Новые товары": "Жаңа тауарлар", + "Новый": "Жаңа", + "Новое": "Жаңа", + "Номер заказа будет присвоен после подтверждения": "Тапсырыс нөмірі растаудан кейін беріледі", + "Номер карты": "Карта нөмірі", + "Ноутбуки": "Ноутбуктер", + "Ноутбуки для работы и игр.": "Жұмыс пен ойынға арналған ноутбуктер.", + "О компании": "Компания туралы", + "О компании: помощь в подборе комплектующих, консультации и поддержка при сборке ПК.": "Компания туралы: бөлшектерді таңдауға көмек, кеңес және ПК жинауға қолдау.", + "О нас": "Біз туралы", + "Обновить": "Жаңарту", + "Обновление": "Жаңарту", + "Объем": "Көлемі", + "Объем видеопамяти": "Бейнежад көлемі", + "Объем кэша": "Кэш көлемі", + "Объем накопителя": "Жинақтауыш көлемі", + "Объем памяти": "Жад көлемі", + "Объясняем условия доставки, оплаты, возврата и гарантии простым языком.": "Жеткізу, төлем, қайтару және кепілдік шарттарын қарапайым тілмен түсіндіреміз.", + "Онлайн-чат": "Онлайн-чат", + "Онлайн‑треккинг": "Онлайн бақылау", + "Описание": "Сипаттама", + "Описание товара будет добавлено позже.": "Тауар сипаттамасы кейін қосылады.", + "Оперативная память": "Жедел жад", + "Оплата": "Төлем", + "Оплата банковской картой": "Банк картасымен төлеу", + "Оплата картой онлайн": "Картамен онлайн төлем", + "Оплата по реквизитам": "Деректемелер бойынша төлем", + "Оплачен": "Төленген", + "Оптимален для игровых сборок.": "Ойын жинақтары үшін оңтайлы.", + "Оставьте заявку — поможем подобрать комплектующие и ответим по доставке.": "Өтінім қалдырыңыз — бөлшектерді таңдауға көмектесіп, жеткізу бойынша жауап береміз.", + "От": "Бастап", + "Открыть заказ": "Тапсырысты ашу", + "Открыть каталог": "Каталогты ашу", + "Отменен": "Бас тартылды", + "Отправить": "Жіберу", + "Отправить сообщение": "Хабарлама жіберу", + "Отправка по стране 1-3 дня": "Ел бойынша жеткізу 1-3 күн", + "Отправлен": "Жіберілген", + "Отличная производительность.": "Жоғары өнімділік.", + "Очистить": "Тазарту", + "Очистить сравнение": "Салыстыруды тазарту", + "Пагинация": "Беттеу", + "Пароль": "Құпиясөз", + "Переведите сумму заказа по реквизитам ниже и подтвердите оформление.": "Төлемді төмендегі деректемелер бойынша аударып, рәсімдеуді растаңыз.", + "Перевод по реквизитам (на карту)": "Деректемелер бойынша аударым (картаға)", + "Перейти в каталог": "Каталогқа өту", + "Перейти к оформлению": "Рәсімдеуге өту", + "Перейти к реквизитам": "Төлем деректеріне өту", + "По вашему запросу ничего не найдено.": "Сұранысыңыз бойынша ештеңе табылмады.", + "По названию": "Атауы бойынша", + "Поддержка": "Қолдау", + "Подробнее": "Толығырақ", + "Подсказываем оптимальные варианты под бюджет и задачи.": "Бюджет пен міндетке сай оңтайлы нұсқаларды ұсынамыз.", + "Подтвердить оформление заказа": "Тапсырысты рәсімдеуді растау", + "Подтверждение оплаты в личном кабинете": "Төлемді жеке кабинетте растау", + "Подтверждение пароля": "Құпиясөзді растау", + "Поиск": "Іздеу", + "Поиск товаров": "Тауар іздеу", + "Поиск товаров по наименованию": "Тауарларды атауы бойынша іздеу", + "Поиск товаров по наименованию: процессоры, видеокарты, материнские платы, ноутбуки и периферия.": "Атауы бойынша тауар іздеу: процессорлар, бейнекарталар, аналық платалар, ноутбуктер және периферия.", + "Поиск: :query": "Іздеу: :query", + "Пока нет заказов.": "Тапсырыстар әлі жоқ.", + "Пока нет новых товаров.": "Жаңа тауарлар әзірге жоқ.", + "Пока нет популярных товаров.": "Танымал тауарлар әзірге жоқ.", + "Пока нет товаров в этой категории.": "Бұл санатта әзірге тауар жоқ.", + "Пока нет товаров.": "Тауарлар әзірге жоқ.", + "Показать": "Көрсету", + "Показать фото :number": ":number суретті көрсету", + "Получатель": "Алушы", + "Покупателю": "Сатып алушыға", + "Поможем с вашей сборкой.": "Жинағыңызды таңдауға көмектесеміз.", + "Популярные товары": "Танымал тауарлар", + "После оплаты отправьте чек в поддержку для подтверждения заказа.": "Төлемнен кейін тапсырысты растау үшін чекті қолдауға жіберіңіз.", + "Почта": "Email", + "Почта:": "Email:", + "Предыдущие товары": "Алдыңғы тауарлар", + "Предыдущий слайд": "Алдыңғы слайд", + "Проверяем ключевые характеристики и совместимость комплектующих.": "Бөлшектердің негізгі сипаттамалары мен үйлесімділігін тексереміз.", + "Продолжить покупки": "Сатып алуды жалғастыру", + "Производитель": "Өндіруші", + "Производитель видеокарты": "Бейнекарта өндірушісі", + "Производитель процессора": "Процессор өндірушісі", + "Процессоры": "Процессорлар", + "Процессоры для игровых и рабочих сборок.": "Ойын және жұмыс жинақтарына арналған процессорлар.", + "Процессоры, видеокарты, материнские платы, ноутбуки и периферия в одном каталоге.": "Процессорлар, бейнекарталар, аналық платалар, ноутбуктер және периферия бір каталогта.", + "Процессоры, видеокарты, материнские платы, ноутбуки и периферия с доставкой по стране.": "Процессорлар, бейнекарталар, аналық платалар, ноутбуктер және периферия ел бойынша жеткізіледі.", + "Процессоры, материнские платы, видеокарты, ноутбуки и периферия в одном каталоге.": "Процессорлар, аналық платалар, бейнекарталар, ноутбуктер және периферия бір каталогта.", + "Разделы сайта": "Сайт бөлімдері", + "Разрешение экрана": "Экран ажыратымдылығы", + "Расскажите о вашей сборке": "Жинағыңыз туралы жазыңыз", + "Рассрочка на крупные заказы": "Ірі тапсырыстарға бөліп төлеу", + "Регистрация": "Тіркелу", + "Результаты по запросу: \":query\"": "\":query\" сұранысы бойынша нәтижелер", + "Результаты поиска": "Іздеу нәтижелері", + "Результаты поиска по запросу «:query». Подберите нужные комплектующие по наименованию.": "«:query» сұранысы бойынша нәтижелер. Қажетті бөлшектерді атауы бойынша таңдаңыз.", + "Результаты поиска: :query": "Іздеу нәтижелері: :query", + "Реквизиты для оплаты": "Төлем деректемелері", + "Сбербанк": "Сбербанк", + "Самовывоз из пункта выдачи": "Беру пунктінен алып кету", + "Сбросить": "Тазарту", + "Свернуть чат": "Чатты жинау", + "Свяжитесь с нами для консультации по вашей сборке.": "Жинағыңыз бойынша кеңес алу үшін бізге хабарласыңыз.", + "Сервис": "Сервис", + "Серебристый": "Күміс түсті", + "Сертификат 80 Plus": "80 Plus сертификаты", + "Синий": "Көк", + "Системы охлаждения": "Салқындату жүйелері", + "Слайд на главной странице": "Басты беттегі слайд", + "Следующие товары": "Келесі тауарлар", + "Следующий слайд": "Келесі слайд", + "Сначала дешевле": "Алдымен арзаны", + "Сначала дороже": "Алдымен қымбаты", + "Сначала заполните данные получателя.": "Алдымен алушы деректерін толтырыңыз.", + "Сначала новые": "Алдымен жаңалары", + "Собирайте ПК быстрее": "ПК-ні тезірек жинаңыз", + "Создайте аккаунт, чтобы отслеживать заказы и сохранять избранное.": "Тапсырыстарды қадағалау және таңдаулыларды сақтау үшін аккаунт жасаңыз.", + "Создать аккаунт": "Аккаунт жасау", + "Сообщение": "Хабарлама", + "Сообщение содержит недопустимые символы.": "Хабарламада рұқсат етілмеген таңбалар бар.", + "Сопровождаем заказ от оформления до получения.": "Тапсырысты рәсімдеуден бастап алғанға дейін бірге жүреміз.", + "Сортировка:": "Сұрыптау:", + "Состав заказа": "Тапсырыс құрамы", + "Сохранить": "Сақтау", + "Список пуст": "Тізім бос", + "Список сравнения очищен.": "Салыстыру тізімі тазартылды.", + "Список сравнения пуст": "Салыстыру тізімі бос", + "Способ оплаты:": "Төлем тәсілі:", + "Способы оплаты": "Төлем тәсілдері", + "Сравнение": "Салыстыру", + "Сравнение товаров": "Тауарларды салыстыру", + "Сравнить": "Салыстыру", + "Стабильная линия питания.": "Тұрақты қорек желісі.", + "Стандарт Wi-Fi": "Wi-Fi стандарты", + "Статус:": "Мәртебе:", + "Сумма": "Сома", + "Сумма заказа:": "Тапсырыс сомасы:", + "Сумма к оплате:": "Төлем сомасы:", + "Система охлаждения": "Салқындату жүйесі", + "Телевизоры": "Теледидарлар", + "Телевизоры для дома и офиса.": "Үй мен офиске арналған теледидарлар.", + "Телефон": "Телефон", + "Телефон:": "Телефон:", + "Тихая работа и охлаждение.": "Тыныш жұмыс және салқындату.", + "Тихий режим работы.": "Тыныш жұмыс режимі.", + "Товар \":name\" добавлен в избранное.": "«:name» тауары таңдаулыларға қосылды.", + "Товар \":name\" добавлен в корзину.": "«:name» тауары себетке қосылды.", + "Товар \":name\" добавлен в сравнение.": "«:name» тауары салыстыруға қосылды.", + "Товар \":name\" удален из избранного.": "«:name» тауары таңдаулылардан алынды.", + "Товар \":name\" удален из корзины.": "«:name» тауары себеттен алынды.", + "Товар \":name\" удален из сравнения.": "«:name» тауары салыстырудан алынды.", + "Товар сейчас недоступен для заказа.": "Бұл тауарға қазір тапсырыс беруге болмайды.", + "Товаров": "Тауар", + "Товары в корзине": "Себеттегі тауарлар", + "Товары категории :category. Фильтры и сортировка для быстрого подбора.": ":category санатындағы тауарлар. Жылдам таңдау үшін сүзгілер мен сұрыптау.", + "Товар": "Тауар", + "Топовый воздушный кулер.": "Үздік ауа кулері.", + "Тип": "Түрі", + "Тип видеопамяти": "Бейнежад түрі", + "Тип матрицы": "Матрица түрі", + "Тип памяти": "Жад түрі", + "Тип поддерживаемой памяти": "Қолдайтын жад түрі", + "Тип процессора": "Процессор түрі", + "Тип сокета": "Сокет түрі", + "Тип устройства": "Құрылғы түрі", + "Типоразмер": "Өлшемі", + "Топовый воздушный кулер": "Үздік ауа кулері", + "Убрать из избранного": "Таңдаулылардан алып тастау", + "Убрать из сравнения": "Салыстырудан алып тастау", + "Удалить": "Жою", + "Уже есть аккаунт": "Аккаунтым бар", + "Узнайте сроки доставки и способы оплаты заказа.": "Жеткізу мерзімдері мен тапсырыс төлеу тәсілдерін біліңіз.", + "Управляйте данными профиля и просматривайте историю заказов.": "Профиль деректерін басқарып, тапсырыстар тарихын қараңыз.", + "Условия доставки и способы оплаты заказов в интернет-магазине комплектующих.": "Бөлшектер интернет-дүкеніндегі жеткізу шарттары мен төлем тәсілдері.", + "Условия доставки, способы оплаты и сроки отправки.": "Жеткізу шарттары, төлем тәсілдері және жөнелту мерзімі.", + "Устройства Apple: ноутбуки, планшеты и смартфоны.": "Apple құрылғылары: ноутбуктер, планшеттер және смартфондар.", + "Фиолетовый": "Күлгін", + "Фильтр": "Сүзгі", + "Фильтры": "Сүзгілер", + "Форм-фактор": "Форм-фактор", + "Характеристика": "Сипаттама", + "Характеристики": "Сипаттамалар", + "Характеристики еще не добавлены.": "Сипаттамалар әлі қосылмаған.", + "Хлебные крошки": "Навигация жолы", + "Цвет": "Түсі", + "Цена": "Баға", + "Цена: :from - :to": "Баға: :from - :to", + "Часы:": "Жұмыс уақыты:", + "Частота": "Жиілігі", + "Частота обновления": "Жаңарту жиілігі", + "Чат": "Чат", + "Чат закрыт администратором. Отправьте новое сообщение, чтобы начать новый диалог.": "Чатты әкімші жапты. Жаңа диалог бастау үшін жаңа хабарлама жіберіңіз.", + "Чем занимаемся и как помогаем выбрать комплектующие.": "Не істейтініміз және жинақтаушыны қалай таңдауға көмектесетініміз.", + "Черный": "Қара", + "Что еще посмотреть в этой категории": "Осы санатта тағы не көруге болады", + "Чипсет": "Чипсет", + "Электронная почта": "Электрондық пошта", + "Ядро": "Ядро", + "о компании, магазин комплектующих, поддержка": "компания туралы, бөлшектер дүкені, қолдау", + "доставка, оплата, условия заказа": "жеткізу, төлем, тапсырыс шарттары", + "интернет-магазин пк, комплектующие, процессоры, видеокарты, ноутбуки": "пк интернет-дүкені, бөлшектер, процессорлар, бейнекарталар, ноутбуктер", + "каталог комплектующих, поиск товаров, процессоры, материнские платы, видеокарты": "бөлшектер каталогы, тауар іздеу, процессорлар, аналық платалар, бейнекарталар", + "контакты магазина, телефон, email, адрес": "дүкен байланыстары, телефон, email, мекенжай", + "поиск товаров, результаты поиска, комплектующие пк, ноутбуки": "тауар іздеу, іздеу нәтижелері, пк бөлшектері, ноутбуктер", + "товар": "тауар", + "Пн-Вс: 10:00-20:00": "Дс-Жс: 10:00-20:00", + "Сбербанк": "Сбербанк", + "ул. Технопарк, 24, Техноград": "Технопарк көшесі, 24, Техноград", + "Материнские платы.": "Аналық платалар.", + "Производитель": "Өндіруші", + "Производитель видеокарты": "Бейнекарта өндірушісі", + "Производитель процессора": "Процессор өндірушісі", + "Память": "Жад", + "Оперативная память": "Жедел жад", + "Системы охлаждения.": "Салқындату жүйелері.", + "Төлем": "Төлем" +} diff --git a/resources/css/app.css b/resources/css/app.css index 7fa1583..51b583d 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -138,12 +138,150 @@ body.pc-body { justify-content: center; } +.pc-header-tools { + display: flex; + align-items: center; +} + .pc-header-icons { display: flex; gap: 14px; align-items: flex-start; } +.pc-lang-switcher { + position: relative; +} + +.pc-lang-switcher-trigger { + list-style: none; + display: inline-flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-height: 40px; + padding: 8px 12px; + border-radius: 999px; + border: 1px solid var(--border); + background: #ffffff; + color: var(--ink); + cursor: pointer; + font-size: 0.82rem; + font-weight: 700; + transition: + border-color 0.2s ease, + background 0.2s ease, + box-shadow 0.2s ease; +} + +.pc-lang-switcher-trigger::-webkit-details-marker { + display: none; +} + +.pc-lang-switcher-trigger:hover { + border-color: rgba(15, 23, 42, 0.2); + background: #f8fafc; +} + +.pc-lang-switcher-trigger:focus-visible { + outline: 2px solid rgba(226, 74, 74, 0.25); + outline-offset: 2px; +} + +.pc-lang-switcher[open] .pc-lang-switcher-trigger { + border-color: rgba(199, 58, 58, 0.24); + box-shadow: 0 0 0 4px rgba(226, 74, 74, 0.08); +} + +.pc-lang-switcher-current { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.pc-lang-switcher-chevron { + width: 18px; + height: 18px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--muted); + transition: transform 0.2s ease, color 0.2s ease; +} + +.pc-lang-switcher-chevron svg { + width: 14px; + height: 14px; + fill: currentColor; +} + +.pc-lang-switcher[open] .pc-lang-switcher-chevron { + transform: rotate(180deg); + color: var(--ink); +} + +.pc-lang-switcher-menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 170px; + padding: 8px; + border-radius: 14px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.98); + box-shadow: var(--shadow); + display: grid; + gap: 6px; + z-index: 120; +} + +.pc-lang-switcher-option { + width: 100%; + display: inline-flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 9px 10px; + border: 0; + border-radius: 10px; + background: transparent; + color: var(--ink); + font: inherit; + text-align: left; + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease; +} + +.pc-lang-switcher-option:hover:not(:disabled) { + background: #f8fafc; +} + +.pc-lang-switcher-option:disabled { + opacity: 1; + cursor: default; +} + +.pc-lang-switcher-option-main { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.pc-lang-switcher-option.is-active { + background: rgba(226, 74, 74, 0.08); + color: #b23434; +} + +.pc-lang-switcher-check { + font-size: 1.1rem; + line-height: 1; +} + +.pc-lang-switcher-flag { + font-size: 1rem; + line-height: 1; +} + .pc-mobile-menu-toggle { display: none; } @@ -2560,14 +2698,18 @@ body.pc-body { letter-spacing: 0.08em; } + .pc-header-tools { + display: none; + } + .pc-catalog-btn { display: none; } .pc-hamburger { display: inline-flex; - order: 2; - margin-left: auto; + order: 3; + margin-left: 0; } .pc-mobile-menu-head { @@ -2610,11 +2752,33 @@ body.pc-body { flex: 0 0 100%; } + .pc-mobile-menu-toggle:checked ~ .pc-header-tools { + display: flex; + order: 3; + margin-left: 0; + width: 100%; + } + + .pc-mobile-menu-toggle:checked ~ .pc-header-tools .pc-lang-switcher { + width: 100%; + } + + .pc-mobile-menu-toggle:checked ~ .pc-header-tools .pc-lang-switcher-trigger { + width: 100%; + } + + .pc-mobile-menu-toggle:checked ~ .pc-header-tools .pc-lang-switcher-menu { + left: 0; + right: auto; + width: 100%; + min-width: 0; + } + .pc-mobile-menu-toggle:checked ~ .pc-header-center { display: flex; flex-direction: column; gap: 10px; - order: 3; + order: 4; width: 100%; padding-top: 0; border-top: 0; @@ -2625,7 +2789,7 @@ body.pc-body { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; - order: 4; + order: 5; width: 100%; } @@ -2634,6 +2798,7 @@ body.pc-body { grid-template-columns: 1fr; gap: 6px; width: 100%; + order: 6; max-height: none; opacity: 1; overflow: visible; diff --git a/resources/js/app.js b/resources/js/app.js index 9964029..615ef7e 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -54,6 +54,8 @@ const initChatWidget = () => { const fetchUrl = widget.dataset.fetchUrl; const sendUrl = widget.dataset.sendUrl; const csrf = widget.dataset.csrf; + const sendLabel = widget.dataset.sendLabel || 'Send'; + const restartLabel = widget.dataset.restartLabel || 'Restart'; const textarea = form.querySelector('textarea[name="message"]'); const submitButton = form.querySelector('button[type="submit"]'); @@ -83,7 +85,7 @@ const initChatWidget = () => { note.textContent = conversationClosed && closedNotice !== '' ? closedNotice : defaultNoteText; } - submitButton.textContent = conversationClosed ? 'Начать новый чат' : 'Отправить'; + submitButton.textContent = conversationClosed ? restartLabel : sendLabel; if (conversationClosed) { stopPolling(); @@ -560,6 +562,46 @@ const initCategoryFilterToggle = () => { }); }; +const initLanguageSwitchers = () => { + const switchers = Array.from(document.querySelectorAll('[data-locale-switcher]')).filter( + (switcher) => switcher instanceof HTMLDetailsElement, + ); + + if (switchers.length === 0) { + return; + } + + document.addEventListener('click', (event) => { + switchers.forEach((switcher) => { + if (!(switcher instanceof HTMLDetailsElement) || !switcher.open) { + return; + } + + if (event.target instanceof Node && switcher.contains(event.target)) { + return; + } + + switcher.open = false; + }); + }); + + switchers.forEach((switcher) => { + const summary = switcher.querySelector('summary'); + + switcher.addEventListener('keydown', (event) => { + if (event.key !== 'Escape') { + return; + } + + switcher.open = false; + + if (summary instanceof HTMLElement) { + summary.focus(); + } + }); + }); +}; + document.addEventListener('submit', (event) => { const form = event.target; if (!(form instanceof HTMLFormElement)) { @@ -620,3 +662,4 @@ initHomeSliders(); initProductCarousels(); initProductGallery(); initCategoryFilterToggle(); +initLanguageSwitchers(); diff --git a/resources/views/components/chat-widget.blade.php b/resources/views/components/chat-widget.blade.php index 0e2ed9d..07364b8 100644 --- a/resources/views/components/chat-widget.blade.php +++ b/resources/views/components/chat-widget.blade.php @@ -4,32 +4,34 @@ data-fetch-url="{{ route('chat.messages') }}" data-send-url="{{ route('chat.send') }}" data-csrf="{{ csrf_token() }}" + data-send-label="{{ __('Отправить') }}" + data-restart-label="{{ __('Начать новый чат') }}" > diff --git a/resources/views/components/footer.blade.php b/resources/views/components/footer.blade.php index f23d948..e1c12cb 100644 --- a/resources/views/components/footer.blade.php +++ b/resources/views/components/footer.blade.php @@ -1,10 +1,10 @@ @php $companyName = config('shop.company_name', config('app.name')); - $companyDescription = config('shop.company_description'); + $companyDescription = __(config('shop.company_description')); $contactPhone = trim((string) config('shop.contact_phone')); $contactEmail = trim((string) config('shop.contact_email')); $contactTelegram = trim((string) config('shop.contact_telegram')); - $contactHours = trim((string) config('shop.contact_hours')); + $contactHours = trim((string) __(config('shop.contact_hours'))); $telegramUrl = ''; $phoneUrl = ''; $emailUrl = ''; @@ -32,28 +32,28 @@ - - + + diff --git a/resources/views/partials/pagination.blade.php b/resources/views/partials/pagination.blade.php index 66f562f..2a2bceb 100644 --- a/resources/views/partials/pagination.blade.php +++ b/resources/views/partials/pagination.blade.php @@ -1,11 +1,11 @@ @if ($paginator->hasPages()) -