AGDNS-3312 Add blocked services group separation

Squashed commit of the following:

commit 0c931c596316aa3322a8123877650a53b2ba31c3
Author: Elizaveta <e.egorova@adguard.com>
Date:   Wed Nov 19 14:14:04 2025 +0300

    spaces

commit 90b8bed07e392c32f2a4c963329563250ca06100
Author: Elizaveta <e.egorova@adguard.com>
Date:   Wed Nov 19 13:55:56 2025 +0300

    update changelog

commit 1a0dea21ccdc82603da4f42bd5396b8cbb29160a
Merge: c70212ec7 ff0ef4f39
Author: Elizaveta <e.egorova@adguard.com>
Date:   Wed Nov 19 13:54:39 2025 +0300

    Merge branch 'master' into AGDNS-3312

commit c70212ec761714528216f5f5dafe1f6683c480b5
Author: Elizaveta <e.egorova@adguard.com>
Date:   Tue Nov 18 17:21:26 2025 +0300

    remove redundant line

commit b08a8d0341fd7a042f63f960c784a11dce915a18
Author: Elizaveta <e.egorova@adguard.com>
Date:   Tue Nov 18 14:02:32 2025 +0300

    lint

commit c78f40bc6986a64b6a8606790b2307076f5b12c8
Author: Elizaveta <e.egorova@adguard.com>
Date:   Tue Nov 18 14:02:07 2025 +0300

    update translations

commit dd53e8b47361fa1b138151dc504fe5ee138391c4
Author: Elizaveta <e.egorova@adguard.com>
Date:   Tue Nov 18 13:55:27 2025 +0300

    remove accordion

commit af2bd3bdfd71d93fcf83d7fa836d85c319293d6d
Author: Elizaveta <e.egorova@adguard.com>
Date:   Fri Nov 14 16:09:22 2025 +0300

    linter

commit 19eb2bfdc12f9c2a4c4f525027811719a72da214
Author: Elizaveta <e.egorova@adguard.com>
Date:   Fri Nov 14 16:07:48 2025 +0300

    fix

commit fc9a32299e778270354ce3fadd046437cda63a0d
Author: Elizaveta <e.egorova@adguard.com>
Date:   Fri Nov 14 14:29:29 2025 +0300

    fix translations

commit 9c4dbc5fdebe00f4aa545172ffd733b71028a243
Author: Elizaveta <e.egorova@adguard.com>
Date:   Thu Nov 13 19:22:51 2025 +0300

    linter

commit 90107c52fa8391294201ecf5cfb437716e7d1eca
Author: Elizaveta <e.egorova@adguard.com>
Date:   Thu Nov 13 18:07:26 2025 +0300

    fix styles

commit 4dd170e91a14dcab5f7b168cdf6f410847ad1dda
Author: Elizaveta <e.egorova@adguard.com>
Date:   Thu Nov 13 18:01:52 2025 +0300

    update translations

commit 1a865f0856
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Wed Nov 12 16:06:19 2025 +0300

    Pull request 2523: AGDNS-3398-imp-translations-script
    
    Squashed commit of the following:
    
    commit 3753e6d308c9dbe23d1c7c41cf57457b364cb253
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Wed Nov 12 15:43:55 2025 +0300
    
        translations: imp code, docs
    
    commit 5389ec67eddbe33a99c3c893cb5b63c131d34e84
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Wed Nov 12 14:14:08 2025 +0300
    
        all: fix lint, imp code
    
    commit c2e1b8edee
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Mon Nov 10 18:03:23 2025 +0300
    
        all: imp translations script
    
    commit c930dcff36
    Author: Eugene Burkov <E.Burkov@AdGuard.COM>
    Date:   Mon Nov 10 17:52:44 2025 +0300
    
        scripts: revert master

commit c28e13b4e1
Author: Elizaveta <e.egorova@adguard.com>
Date:   Wed Nov 5 16:43:13 2025 +0300

    move project key const

commit ee977f5993
Author: Elizaveta <e.egorova@adguard.com>
Date:   Wed Nov 5 16:25:34 2025 +0300

    update download script

commit 244c1011b0
Author: Elizaveta <e.egorova@adguard.com>
Date:   Sat Nov 1 15:59:04 2025 +0300

    restore master toggle

commit 40418ea8cf
Author: Elizaveta <e.egorova@adguard.com>
Date:   Sat Nov 1 14:59:57 2025 +0300

    update saveToFile func

commit e4f3c677d4
Author: Elizaveta <e.egorova@adguard.com>
Date:   Sat Nov 1 14:52:14 2025 +0300

    go linter

commit 5359590953
Author: Elizaveta <e.egorova@adguard.com>
Date:   Sat Nov 1 14:44:39 2025 +0300

    remove en group tr from __locales

commit a91215dfad
Author: Elizaveta <e.egorova@adguard.com>
Date:   Sat Nov 1 14:43:39 2025 +0300

    add translations

commit c65f80048d
Author: Elizaveta <e.egorova@adguard.com>
Date:   Sat Nov 1 12:15:07 2025 +0300

    fix ui

commit 893433bd8f
Author: Elizaveta <e.egorova@adguard.com>
Date:   Fri Oct 31 17:22:44 2025 +0300

    fix comment

commit 26148996c9
Author: Elizaveta <e.egorova@adguard.com>
Date:   Fri Oct 31 16:49:46 2025 +0300

    styles

commit 2785958252
Author: Elizaveta <e.egorova@adguard.com>
Date:   Fri Oct 31 16:48:46 2025 +0300

    linter

... and 6 more commits
This commit is contained in:
Elizaveta Egorova
2025-11-19 14:29:26 +03:00
parent ff0ef4f398
commit b00d982884
48 changed files with 2025 additions and 247 deletions

View File

@@ -43,5 +43,50 @@
"zh-hk": "繁體中文(香港)",
"zh-tw": "正體中文(台灣)"
}
},
{
"project_id": "hostlists-registry",
"base_locale": "en",
"localizable_files": [
"client/src/__locales-services/services.json"
],
"languages": {
"ar": "العربية",
"be": "Беларуская",
"bg": "Български",
"cs": "Český",
"da": "Dansk",
"de": "Deutsch",
"en": "English",
"es": "Español",
"fa": "فارسی",
"fi": "Suomi",
"fr": "Français",
"hr": "Hrvatski",
"hu": "Magyar",
"id": "Indonesian",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nl": "Nederlands",
"no": "Norsk",
"pl": "Polski",
"pt-br": "Português (BR)",
"pt-pt": "Português (PT)",
"ro": "Română",
"ru": "Русский",
"si-lk": "සිංහල",
"sk": "Slovenčina",
"sl": "Slovenščina",
"sr-cs": "Srpski",
"sv": "Svenska",
"th": "ภาษาไทย",
"tr": "Türkçe",
"uk": "Українська",
"vi": "Tiếng Việt",
"zh-cn": "简体中文",
"zh-hk": "繁體中文(香港)",
"zh-tw": "正體中文(台灣)"
}
}
]

View File

@@ -18,14 +18,18 @@ See also the [v0.107.70 GitHub milestone][ms-v0.107.70].
NOTE: Add new changes BELOW THIS COMMENT.
-->
### Fixed
- Generated mobileconfig could not be installed on macOS 26.1.
### Added
- New field `"start_time"` in the `GET /control/status` response.
### Changed
- New blocked services UI.
### Fixed
- Generated mobileconfig could not be installed on macOS 26.1.
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "الذكاء الاصطناعي"
},
"servicesgroup.cdn.name": {
"message": "شبكات توصيل المحتوى (CDN)"
},
"servicesgroup.dating.name": {
"message": "خدمات المواعدة"
},
"servicesgroup.gambling.name": {
"message": "القمار والمراهنة"
},
"servicesgroup.gaming.name": {
"message": "الألعاب الرقمية"
},
"servicesgroup.hosting.name": {
"message": "استضافة الويب"
},
"servicesgroup.messenger.name": {
"message": "خدمات المراسلة"
},
"servicesgroup.privacy.name": {
"message": "أدوات الخصوصية"
},
"servicesgroup.shopping.name": {
"message": "التسوق"
},
"servicesgroup.social_network.name": {
"message": "شبكات التواصل الإجتماعية"
},
"servicesgroup.software.name": {
"message": "تطوير البرمجيات"
},
"servicesgroup.streaming.name": {
"message": "خدمات البث المباشر"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Штучны інтэлект"
},
"servicesgroup.cdn.name": {
"message": "Сеткі дастаўкі кантэнту (CDN)"
},
"servicesgroup.dating.name": {
"message": "Сэрвісы знаёмстваў"
},
"servicesgroup.gambling.name": {
"message": "Азартныя гульні і стаўкі"
},
"servicesgroup.gaming.name": {
"message": "Гэймінг"
},
"servicesgroup.hosting.name": {
"message": "Вэб-хостынг"
},
"servicesgroup.messenger.name": {
"message": "Сэрвісы паведамленняў"
},
"servicesgroup.privacy.name": {
"message": "Інструменты прыватнасці"
},
"servicesgroup.shopping.name": {
"message": "Крамы"
},
"servicesgroup.social_network.name": {
"message": "Сацыяльныя сеткі"
},
"servicesgroup.software.name": {
"message": "Распрацоўка праграмнага забеспячэння"
},
"servicesgroup.streaming.name": {
"message": "Стрымінгавыя сэрвісы"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Изкуствен интелект"
},
"servicesgroup.cdn.name": {
"message": "Мрежи за доставяне на съдържание (CDN)"
},
"servicesgroup.dating.name": {
"message": "Служби за запознанства"
},
"servicesgroup.gambling.name": {
"message": "Хазарт и залагания"
},
"servicesgroup.gaming.name": {
"message": "Гейминг"
},
"servicesgroup.hosting.name": {
"message": "Уеб хостинг"
},
"servicesgroup.messenger.name": {
"message": "Услуги за съобщения"
},
"servicesgroup.privacy.name": {
"message": "Инструменти за поверителност"
},
"servicesgroup.shopping.name": {
"message": "Пазаруване"
},
"servicesgroup.social_network.name": {
"message": "Социални мрежи"
},
"servicesgroup.software.name": {
"message": "Разработка на софтуер"
},
"servicesgroup.streaming.name": {
"message": "Стрийминг услуги"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Umělá inteligence"
},
"servicesgroup.cdn.name": {
"message": "Sítě pro doručování obsahu (CDN)"
},
"servicesgroup.dating.name": {
"message": "Seznamovací služby"
},
"servicesgroup.gambling.name": {
"message": "Hazardní hry a sázení"
},
"servicesgroup.gaming.name": {
"message": "Hraní"
},
"servicesgroup.hosting.name": {
"message": "Webhosting"
},
"servicesgroup.messenger.name": {
"message": "Služby pro zasílání zpráv"
},
"servicesgroup.privacy.name": {
"message": "Nástroje pro ochranu soukromí"
},
"servicesgroup.shopping.name": {
"message": "Nakupování"
},
"servicesgroup.social_network.name": {
"message": "Sociální sítě"
},
"servicesgroup.software.name": {
"message": "Vývoj softwaru"
},
"servicesgroup.streaming.name": {
"message": "Streamovací služby"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Kunstig intelligens"
},
"servicesgroup.cdn.name": {
"message": "Indholdsleveringsnetværk (CDN)"
},
"servicesgroup.dating.name": {
"message": "Dating-tjenester"
},
"servicesgroup.gambling.name": {
"message": "Spil og væddemål"
},
"servicesgroup.gaming.name": {
"message": "Gaming"
},
"servicesgroup.hosting.name": {
"message": "Webhosting"
},
"servicesgroup.messenger.name": {
"message": "Beskedtjenester"
},
"servicesgroup.privacy.name": {
"message": "Fortrolighedsværktøjer"
},
"servicesgroup.shopping.name": {
"message": "Shopping"
},
"servicesgroup.social_network.name": {
"message": "Sociale netværk"
},
"servicesgroup.software.name": {
"message": "Softwareudvikling"
},
"servicesgroup.streaming.name": {
"message": "Streamingtjenester"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Künstliche Intelligenz"
},
"servicesgroup.cdn.name": {
"message": "Content-Delivery-Netzwerke (CDN)"
},
"servicesgroup.dating.name": {
"message": "Dating-Dienste"
},
"servicesgroup.gambling.name": {
"message": "Glücksspiel und Wetten"
},
"servicesgroup.gaming.name": {
"message": "Spiele"
},
"servicesgroup.hosting.name": {
"message": "Webhosting"
},
"servicesgroup.messenger.name": {
"message": "Messaging-Dienste"
},
"servicesgroup.privacy.name": {
"message": "Datenschutz-Tools"
},
"servicesgroup.shopping.name": {
"message": "Einkaufen"
},
"servicesgroup.social_network.name": {
"message": "Soziale Netzwerke"
},
"servicesgroup.software.name": {
"message": "Softwareentwicklung"
},
"servicesgroup.streaming.name": {
"message": "Streaming-Dienste"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Artificial intelligence"
},
"servicesgroup.cdn.name": {
"message": "Content delivery networks (CDN)"
},
"servicesgroup.dating.name": {
"message": "Dating services"
},
"servicesgroup.gambling.name": {
"message": "Gambling and betting"
},
"servicesgroup.gaming.name": {
"message": "Gaming"
},
"servicesgroup.hosting.name": {
"message": "Web hosting"
},
"servicesgroup.messenger.name": {
"message": "Messaging services"
},
"servicesgroup.privacy.name": {
"message": "Privacy tools"
},
"servicesgroup.shopping.name": {
"message": "Shopping"
},
"servicesgroup.social_network.name": {
"message": "Social networks"
},
"servicesgroup.software.name": {
"message": "Software development"
},
"servicesgroup.streaming.name": {
"message": "Streaming services"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Inteligencia artificial"
},
"servicesgroup.cdn.name": {
"message": "Redes de entrega de contenido (CDN)"
},
"servicesgroup.dating.name": {
"message": "Servicios de citas"
},
"servicesgroup.gambling.name": {
"message": "Juegos de azar y apuestas"
},
"servicesgroup.gaming.name": {
"message": "Juegos"
},
"servicesgroup.hosting.name": {
"message": "Web hosting"
},
"servicesgroup.messenger.name": {
"message": "Servicios de mensajería"
},
"servicesgroup.privacy.name": {
"message": "Herramientas de privacidad"
},
"servicesgroup.shopping.name": {
"message": "Compras"
},
"servicesgroup.social_network.name": {
"message": "Redes sociales"
},
"servicesgroup.software.name": {
"message": "Desarrollo de software"
},
"servicesgroup.streaming.name": {
"message": "Servicios de streaming"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "هوش مصنوعی"
},
"servicesgroup.cdn.name": {
"message": "شبکه‌های تحویل محتوا (CDN)"
},
"servicesgroup.dating.name": {
"message": "خدمات دوستیابی"
},
"servicesgroup.gambling.name": {
"message": "قمار و شرط‌بندی"
},
"servicesgroup.gaming.name": {
"message": "بازی"
},
"servicesgroup.hosting.name": {
"message": "میزبانی وب"
},
"servicesgroup.messenger.name": {
"message": "خدمات پیام رسان"
},
"servicesgroup.privacy.name": {
"message": "ابزارهای حریم خصوصی"
},
"servicesgroup.shopping.name": {
"message": "خرید"
},
"servicesgroup.social_network.name": {
"message": "شبکه اجتماعی"
},
"servicesgroup.software.name": {
"message": "توسعه نرمافزار"
},
"servicesgroup.streaming.name": {
"message": "خدمات استریم"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Tekoäly"
},
"servicesgroup.cdn.name": {
"message": "Sisällönjakeluverkot (CDN)"
},
"servicesgroup.dating.name": {
"message": "Deittipalvelut"
},
"servicesgroup.gambling.name": {
"message": "Uhkapelit ja vedonlyönti"
},
"servicesgroup.gaming.name": {
"message": "Pelaaminen"
},
"servicesgroup.hosting.name": {
"message": "Web-hosting"
},
"servicesgroup.messenger.name": {
"message": "Viestipalvelut"
},
"servicesgroup.privacy.name": {
"message": "Yksityisyyden työkalut"
},
"servicesgroup.shopping.name": {
"message": "Verkkokaupat"
},
"servicesgroup.social_network.name": {
"message": "Sosiaaliset verkostot"
},
"servicesgroup.software.name": {
"message": "Ohjelmistokehitys"
},
"servicesgroup.streaming.name": {
"message": "Suoratoistopalvelut"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Intelligence artificielle"
},
"servicesgroup.cdn.name": {
"message": "Réseaux de distribution de contenu (CDN)"
},
"servicesgroup.dating.name": {
"message": "Services de rencontres"
},
"servicesgroup.gambling.name": {
"message": "Jeux de hasard et paris sportifs"
},
"servicesgroup.gaming.name": {
"message": "Jeux vidéo"
},
"servicesgroup.hosting.name": {
"message": "Hébergement Web"
},
"servicesgroup.messenger.name": {
"message": "Services de messagerie"
},
"servicesgroup.privacy.name": {
"message": "Outils de confidentialité"
},
"servicesgroup.shopping.name": {
"message": "Shopping"
},
"servicesgroup.social_network.name": {
"message": "Réseaux sociaux"
},
"servicesgroup.software.name": {
"message": "Développement de logiciels"
},
"servicesgroup.streaming.name": {
"message": "Services de streaming"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Umjetna inteligencija"
},
"servicesgroup.cdn.name": {
"message": "Mreže za isporuku sadržaja (CDN)"
},
"servicesgroup.dating.name": {
"message": "Usluge za upoznavanje"
},
"servicesgroup.gambling.name": {
"message": "Kockanje i klađenje"
},
"servicesgroup.gaming.name": {
"message": "Igre"
},
"servicesgroup.hosting.name": {
"message": "Web hosting"
},
"servicesgroup.messenger.name": {
"message": "Usluge razmjene poruka"
},
"servicesgroup.privacy.name": {
"message": "Alati za privatnost"
},
"servicesgroup.shopping.name": {
"message": "Kupovina"
},
"servicesgroup.social_network.name": {
"message": "Društvene mreže"
},
"servicesgroup.software.name": {
"message": "Razvoj softwarea"
},
"servicesgroup.streaming.name": {
"message": "Usluge streaminga"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Mesterséges intelligencia"
},
"servicesgroup.cdn.name": {
"message": "Tartalomszolgáltató hálózatok (CDN)"
},
"servicesgroup.dating.name": {
"message": "Társkereső szolgáltatások"
},
"servicesgroup.gambling.name": {
"message": "Szerencsejáték és fogadás"
},
"servicesgroup.gaming.name": {
"message": "Játék"
},
"servicesgroup.hosting.name": {
"message": "Webtárhely"
},
"servicesgroup.messenger.name": {
"message": "Üzenetküldő szolgáltatások"
},
"servicesgroup.privacy.name": {
"message": "Adatvédelmi eszközök"
},
"servicesgroup.shopping.name": {
"message": "Vásárlás"
},
"servicesgroup.social_network.name": {
"message": "Közösségi háló"
},
"servicesgroup.software.name": {
"message": "Szoftverfejlesztés"
},
"servicesgroup.streaming.name": {
"message": "Streaming szolgáltatások"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Kecerdasan buatan"
},
"servicesgroup.cdn.name": {
"message": "Jaringan pengiriman konten (CDN)"
},
"servicesgroup.dating.name": {
"message": "Layanan kencan"
},
"servicesgroup.gambling.name": {
"message": "Perjudian dan taruhan"
},
"servicesgroup.gaming.name": {
"message": "Permainan"
},
"servicesgroup.hosting.name": {
"message": "Hosting web"
},
"servicesgroup.messenger.name": {
"message": "Layanan pesan"
},
"servicesgroup.privacy.name": {
"message": "Alat privasi"
},
"servicesgroup.shopping.name": {
"message": "Belanja"
},
"servicesgroup.social_network.name": {
"message": "Jaringan sosial"
},
"servicesgroup.software.name": {
"message": "Pengembangan software"
},
"servicesgroup.streaming.name": {
"message": "Layanan siar"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Intelligenza artificiale"
},
"servicesgroup.cdn.name": {
"message": "Reti dedicate alla distribuzione dei contenuti (CDN)"
},
"servicesgroup.dating.name": {
"message": "Servizi di incontri"
},
"servicesgroup.gambling.name": {
"message": "Giochi d'azzardo e scommesse"
},
"servicesgroup.gaming.name": {
"message": "Giochi"
},
"servicesgroup.hosting.name": {
"message": "Hosting web"
},
"servicesgroup.messenger.name": {
"message": "Servizi di messaggistica"
},
"servicesgroup.privacy.name": {
"message": "Strumenti per riservatezza"
},
"servicesgroup.shopping.name": {
"message": "Compere"
},
"servicesgroup.social_network.name": {
"message": "Reti sociali"
},
"servicesgroup.software.name": {
"message": "Sviluppo di programmi"
},
"servicesgroup.streaming.name": {
"message": "Servizi di streaming"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "人工知能AI"
},
"servicesgroup.cdn.name": {
"message": "コンテンツ配信ネットワークCDN"
},
"servicesgroup.dating.name": {
"message": "出会い系サービス"
},
"servicesgroup.gambling.name": {
"message": "ギャンブルおよび賭博"
},
"servicesgroup.gaming.name": {
"message": "ゲーム"
},
"servicesgroup.hosting.name": {
"message": "ウェブホスティング"
},
"servicesgroup.messenger.name": {
"message": "メッセージサービス"
},
"servicesgroup.privacy.name": {
"message": "プライバシーツール"
},
"servicesgroup.shopping.name": {
"message": "ショッピング"
},
"servicesgroup.social_network.name": {
"message": "SNS"
},
"servicesgroup.software.name": {
"message": "ソフトウェア開発"
},
"servicesgroup.streaming.name": {
"message": "ストリーミングサービス"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "인공지능"
},
"servicesgroup.cdn.name": {
"message": "콘텐츠 전송 네트워크(CDN)"
},
"servicesgroup.dating.name": {
"message": "데이트 서비스"
},
"servicesgroup.gambling.name": {
"message": "도박 및 베팅"
},
"servicesgroup.gaming.name": {
"message": "게임"
},
"servicesgroup.hosting.name": {
"message": "웹 호스팅"
},
"servicesgroup.messenger.name": {
"message": "메시징 서비스"
},
"servicesgroup.privacy.name": {
"message": "개인정보 보호 도구"
},
"servicesgroup.shopping.name": {
"message": "쇼핑"
},
"servicesgroup.social_network.name": {
"message": "소셜 네트워크"
},
"servicesgroup.software.name": {
"message": "소프트웨어 개발"
},
"servicesgroup.streaming.name": {
"message": "스트리밍 서비스"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Kunstmatige intelligentie"
},
"servicesgroup.cdn.name": {
"message": "Content Delivery Networks (CDN)"
},
"servicesgroup.dating.name": {
"message": "Datingdiensten"
},
"servicesgroup.gambling.name": {
"message": "Gokken en wedden"
},
"servicesgroup.gaming.name": {
"message": "Gamen"
},
"servicesgroup.hosting.name": {
"message": "Webhosting"
},
"servicesgroup.messenger.name": {
"message": "Berichtendiensten"
},
"servicesgroup.privacy.name": {
"message": "Privacyhulpmiddelen"
},
"servicesgroup.shopping.name": {
"message": "Winkelen"
},
"servicesgroup.social_network.name": {
"message": "Sociale netwerken"
},
"servicesgroup.software.name": {
"message": "Softwareontwikkeling"
},
"servicesgroup.streaming.name": {
"message": "Streamingdiensten"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Kunstig intelligens"
},
"servicesgroup.cdn.name": {
"message": "Nettverk for innholdslevering (CDN)"
},
"servicesgroup.dating.name": {
"message": "Datingtjenester"
},
"servicesgroup.gambling.name": {
"message": "Gambling og tipping"
},
"servicesgroup.gaming.name": {
"message": "Spilling"
},
"servicesgroup.hosting.name": {
"message": "Webhotell"
},
"servicesgroup.messenger.name": {
"message": "Meldingstjenester"
},
"servicesgroup.privacy.name": {
"message": "Personvernverktøy"
},
"servicesgroup.shopping.name": {
"message": "Handling"
},
"servicesgroup.social_network.name": {
"message": "Sosiale nettverk"
},
"servicesgroup.software.name": {
"message": "Programvareutvikling"
},
"servicesgroup.streaming.name": {
"message": "Strømmetjenester"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Sztuczna inteligencja"
},
"servicesgroup.cdn.name": {
"message": "Sieci dostarczania treści (CDN)"
},
"servicesgroup.dating.name": {
"message": "Serwisy randkowe"
},
"servicesgroup.gambling.name": {
"message": "Hazard i zakłady"
},
"servicesgroup.gaming.name": {
"message": "Gier"
},
"servicesgroup.hosting.name": {
"message": "Hosting web"
},
"servicesgroup.messenger.name": {
"message": "Usługi przesyłania wiadomości"
},
"servicesgroup.privacy.name": {
"message": "Narzędzia prywatności"
},
"servicesgroup.shopping.name": {
"message": "Zakupy"
},
"servicesgroup.social_network.name": {
"message": "Portale społecznościowe"
},
"servicesgroup.software.name": {
"message": "Rozwój oprogramowania"
},
"servicesgroup.streaming.name": {
"message": "Serwisy streamingowe"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Inteligência artificial"
},
"servicesgroup.cdn.name": {
"message": "Redes de entrega de conteúdo (CDN)"
},
"servicesgroup.dating.name": {
"message": "Serviços de namoro"
},
"servicesgroup.gambling.name": {
"message": "Jogos de azar e apostas"
},
"servicesgroup.gaming.name": {
"message": "Jogos digitais"
},
"servicesgroup.hosting.name": {
"message": "Hospedagem web"
},
"servicesgroup.messenger.name": {
"message": "Serviços de mensagens"
},
"servicesgroup.privacy.name": {
"message": "Ferramentas de privacidade"
},
"servicesgroup.shopping.name": {
"message": "Compras"
},
"servicesgroup.social_network.name": {
"message": "Redes sociais"
},
"servicesgroup.software.name": {
"message": "Desenvolvimento de software"
},
"servicesgroup.streaming.name": {
"message": "Serviços de streaming"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Inteligência artificial"
},
"servicesgroup.cdn.name": {
"message": "Redes de distribuição de conteúdos (CDN)"
},
"servicesgroup.dating.name": {
"message": "Serviços de encontros"
},
"servicesgroup.gambling.name": {
"message": "Jogos de azar e apostas"
},
"servicesgroup.gaming.name": {
"message": "Jogos"
},
"servicesgroup.hosting.name": {
"message": "Alojamento web"
},
"servicesgroup.messenger.name": {
"message": "Serviços de mensagens"
},
"servicesgroup.privacy.name": {
"message": "Ferramentas de privacidade"
},
"servicesgroup.shopping.name": {
"message": "Compras"
},
"servicesgroup.social_network.name": {
"message": "Redes sociais"
},
"servicesgroup.software.name": {
"message": "Desenvolvimento de software"
},
"servicesgroup.streaming.name": {
"message": "Serviços de streaming"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Inteligenţă artificială"
},
"servicesgroup.cdn.name": {
"message": "Rețele de livrare a conținutului (CDN)"
},
"servicesgroup.dating.name": {
"message": "Servicii de întâlniri"
},
"servicesgroup.gambling.name": {
"message": "Jocuri de noroc și pariuri"
},
"servicesgroup.gaming.name": {
"message": "Jocuri"
},
"servicesgroup.hosting.name": {
"message": "Găzduire web"
},
"servicesgroup.messenger.name": {
"message": "Servicii de mesagerie"
},
"servicesgroup.privacy.name": {
"message": "Instrumente de confidențialitate"
},
"servicesgroup.shopping.name": {
"message": "Cumpărături"
},
"servicesgroup.social_network.name": {
"message": "Rețele sociale"
},
"servicesgroup.software.name": {
"message": "Dezvoltare de program"
},
"servicesgroup.streaming.name": {
"message": "Servicii de streaming"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Искусственный интеллект"
},
"servicesgroup.cdn.name": {
"message": "Сети доставки контента (CDN)"
},
"servicesgroup.dating.name": {
"message": "Сервисы знакомств"
},
"servicesgroup.gambling.name": {
"message": "Азартные игры и ставки"
},
"servicesgroup.gaming.name": {
"message": "Игры"
},
"servicesgroup.hosting.name": {
"message": "Веб-хостинг"
},
"servicesgroup.messenger.name": {
"message": "Сервисы обмена сообщениями"
},
"servicesgroup.privacy.name": {
"message": "Инструменты конфиденциальности"
},
"servicesgroup.shopping.name": {
"message": "Шопинг"
},
"servicesgroup.social_network.name": {
"message": "Социальные сети"
},
"servicesgroup.software.name": {
"message": "Разработка программного обеспечения"
},
"servicesgroup.streaming.name": {
"message": "Стриминговые сервисы"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "කෘතිම බුද්ධිය"
},
"servicesgroup.cdn.name": {
"message": "අන්තර්ගත බෙදාහැරීමේ ජාල (CDN)"
},
"servicesgroup.dating.name": {
"message": "ආලය සේවා"
},
"servicesgroup.gambling.name": {
"message": "සූදුව සහ ඔට්ටු ඇල්ලීම"
},
"servicesgroup.gaming.name": {
"message": "ක්‍රීඩා කිරීම"
},
"servicesgroup.hosting.name": {
"message": "වෙබ් සත්කාරකත්වය"
},
"servicesgroup.messenger.name": {
"message": "පණිවිඩ සේවා"
},
"servicesgroup.privacy.name": {
"message": "රහස්‍යතා මෙවලම්"
},
"servicesgroup.shopping.name": {
"message": "සාප්පුයාම"
},
"servicesgroup.social_network.name": {
"message": "සමාජ ජාල"
},
"servicesgroup.software.name": {
"message": "මෘදුකාංග සංවර්ධනය"
},
"servicesgroup.streaming.name": {
"message": "ප්‍රවාහ සේවා"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Umelá inteligencia"
},
"servicesgroup.cdn.name": {
"message": "Siete na doručovanie obsahu (CDN)"
},
"servicesgroup.dating.name": {
"message": "Zoznamovacie služby"
},
"servicesgroup.gambling.name": {
"message": "Hazardné hry a stávkovanie"
},
"servicesgroup.gaming.name": {
"message": "Hranie hier"
},
"servicesgroup.hosting.name": {
"message": "Webhosting"
},
"servicesgroup.messenger.name": {
"message": "Služby zasielania správ"
},
"servicesgroup.privacy.name": {
"message": "Nástroje na ochranu súkromia"
},
"servicesgroup.shopping.name": {
"message": "Nakupovanie"
},
"servicesgroup.social_network.name": {
"message": "Sociálne siete"
},
"servicesgroup.software.name": {
"message": "Vývoj softvéru"
},
"servicesgroup.streaming.name": {
"message": "Streamovacie služby"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Umetna inteligenca"
},
"servicesgroup.cdn.name": {
"message": "Omrežja za dostavo vsebin (CDN)"
},
"servicesgroup.dating.name": {
"message": "Storitve za zmenke"
},
"servicesgroup.gambling.name": {
"message": "Igre na srečo in stave"
},
"servicesgroup.gaming.name": {
"message": "Igre"
},
"servicesgroup.hosting.name": {
"message": "Spletno gostovanje"
},
"servicesgroup.messenger.name": {
"message": "Storitve sporočanja"
},
"servicesgroup.privacy.name": {
"message": "Orodja za zasebnost"
},
"servicesgroup.shopping.name": {
"message": "Nakupi"
},
"servicesgroup.social_network.name": {
"message": "Družbeno omrežje"
},
"servicesgroup.software.name": {
"message": "Razvoj programske opreme"
},
"servicesgroup.streaming.name": {
"message": "Storitve pretakanja"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Veštačka inteligencija"
},
"servicesgroup.cdn.name": {
"message": "Mreže za isporuku sadržaja (CDN)"
},
"servicesgroup.dating.name": {
"message": "Servisi za upoznavanje"
},
"servicesgroup.gambling.name": {
"message": "Kockanje i klađenje"
},
"servicesgroup.gaming.name": {
"message": "Igre"
},
"servicesgroup.hosting.name": {
"message": "Veb hosting"
},
"servicesgroup.messenger.name": {
"message": "Servisi za razmenu poruka"
},
"servicesgroup.privacy.name": {
"message": "Alati za privatnost"
},
"servicesgroup.shopping.name": {
"message": "Kupovina"
},
"servicesgroup.social_network.name": {
"message": "Društvene mreže"
},
"servicesgroup.software.name": {
"message": "Razvoj softvera"
},
"servicesgroup.streaming.name": {
"message": "Streaming servisi"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Artificiell intelligens"
},
"servicesgroup.cdn.name": {
"message": "Innehållsleveransnätverk (CDN)"
},
"servicesgroup.dating.name": {
"message": "Dejtingtjänster"
},
"servicesgroup.gambling.name": {
"message": "Spel och vadslagning"
},
"servicesgroup.gaming.name": {
"message": "Gaming"
},
"servicesgroup.hosting.name": {
"message": "Webbhotell"
},
"servicesgroup.messenger.name": {
"message": "Meddelandetjänster"
},
"servicesgroup.privacy.name": {
"message": "Integritetsverktyg"
},
"servicesgroup.shopping.name": {
"message": "Shopping"
},
"servicesgroup.social_network.name": {
"message": "Sociala nätverk"
},
"servicesgroup.software.name": {
"message": "Programvaruutveckling"
},
"servicesgroup.streaming.name": {
"message": "Streamingtjänster"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "ปัญญาประดิษฐ์"
},
"servicesgroup.cdn.name": {
"message": "เครือข่ายการจัดส่งเนื้อหา (CDN)"
},
"servicesgroup.dating.name": {
"message": "บริการหาคู่"
},
"servicesgroup.gambling.name": {
"message": "การพนันและการเดิมพัน"
},
"servicesgroup.gaming.name": {
"message": "การเล่นเกม"
},
"servicesgroup.hosting.name": {
"message": "เว็บโฮสติ้ง"
},
"servicesgroup.messenger.name": {
"message": "บริการส่งข้อความ"
},
"servicesgroup.privacy.name": {
"message": "เครื่องมือความเป็นส่วนตัว"
},
"servicesgroup.shopping.name": {
"message": "ช้อปปิ้ง"
},
"servicesgroup.social_network.name": {
"message": "สังคมออนไลน์"
},
"servicesgroup.software.name": {
"message": "การพัฒนาซอฟต์แวร์"
},
"servicesgroup.streaming.name": {
"message": "บริการสตรีมมิ่ง"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Yapay zekâ"
},
"servicesgroup.cdn.name": {
"message": "İçerik dağıtım ağları (CDN)"
},
"servicesgroup.dating.name": {
"message": "Flört hizmetleri"
},
"servicesgroup.gambling.name": {
"message": "Kumar ve bahis"
},
"servicesgroup.gaming.name": {
"message": "Oyun"
},
"servicesgroup.hosting.name": {
"message": "Web barındırma"
},
"servicesgroup.messenger.name": {
"message": "Mesajlaşma hizmetleri"
},
"servicesgroup.privacy.name": {
"message": "Gizlilik araçları"
},
"servicesgroup.shopping.name": {
"message": "Alışveriş"
},
"servicesgroup.social_network.name": {
"message": "Sosyal ağlar"
},
"servicesgroup.software.name": {
"message": "Yazılım geliştirme"
},
"servicesgroup.streaming.name": {
"message": "Canlı yayın akışı hizmetleri"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Штучний інтелект"
},
"servicesgroup.cdn.name": {
"message": "Мережі доставки контенту (CDN)"
},
"servicesgroup.dating.name": {
"message": "Сервіси знайомств"
},
"servicesgroup.gambling.name": {
"message": "Азартні ігри та ставки"
},
"servicesgroup.gaming.name": {
"message": "Геймінг"
},
"servicesgroup.hosting.name": {
"message": "Вебхостинг"
},
"servicesgroup.messenger.name": {
"message": "Сервіси обміну повідомленнями"
},
"servicesgroup.privacy.name": {
"message": "Інструменти конфіденційності"
},
"servicesgroup.shopping.name": {
"message": "Магазини"
},
"servicesgroup.social_network.name": {
"message": "Соціальні мережі"
},
"servicesgroup.software.name": {
"message": "Розробка програмного забезпечення"
},
"servicesgroup.streaming.name": {
"message": "Стримінги"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "Trí tuệ nhân tạo"
},
"servicesgroup.cdn.name": {
"message": "Mạng phân phối nội dung (CDN)"
},
"servicesgroup.dating.name": {
"message": "Dịch vụ hẹn hò"
},
"servicesgroup.gambling.name": {
"message": "Cờ bạc và cá cược"
},
"servicesgroup.gaming.name": {
"message": "Chơi game"
},
"servicesgroup.hosting.name": {
"message": "Lưu trữ web"
},
"servicesgroup.messenger.name": {
"message": "Dịch vụ nhắn tin"
},
"servicesgroup.privacy.name": {
"message": "Công cụ bảo mật"
},
"servicesgroup.shopping.name": {
"message": "Mua sắm"
},
"servicesgroup.social_network.name": {
"message": "Mạng xã hội"
},
"servicesgroup.software.name": {
"message": "Phát triển phần mềm"
},
"servicesgroup.streaming.name": {
"message": "Dịch vụ phát trực tuyến"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "人工智能"
},
"servicesgroup.cdn.name": {
"message": "内容分发网络CDN"
},
"servicesgroup.dating.name": {
"message": "交友服务"
},
"servicesgroup.gambling.name": {
"message": "赌博和博彩"
},
"servicesgroup.gaming.name": {
"message": "游戏"
},
"servicesgroup.hosting.name": {
"message": "网站托管"
},
"servicesgroup.messenger.name": {
"message": "消息服务"
},
"servicesgroup.privacy.name": {
"message": "隐私工具"
},
"servicesgroup.shopping.name": {
"message": "购物"
},
"servicesgroup.social_network.name": {
"message": "社交网络"
},
"servicesgroup.software.name": {
"message": "软件开发"
},
"servicesgroup.streaming.name": {
"message": "串流服务"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "人工智能"
},
"servicesgroup.cdn.name": {
"message": "內容傳遞網路 (CDN)"
},
"servicesgroup.dating.name": {
"message": "約會服務"
},
"servicesgroup.gambling.name": {
"message": "賭博和投注"
},
"servicesgroup.gaming.name": {
"message": "遊戲"
},
"servicesgroup.hosting.name": {
"message": "網頁寄存"
},
"servicesgroup.messenger.name": {
"message": "訊息傳送服務"
},
"servicesgroup.privacy.name": {
"message": "隱私權工具"
},
"servicesgroup.shopping.name": {
"message": "購物"
},
"servicesgroup.social_network.name": {
"message": "社交媒體"
},
"servicesgroup.software.name": {
"message": "軟體開發"
},
"servicesgroup.streaming.name": {
"message": "串流服務"
}
}

View File

@@ -0,0 +1,38 @@
{
"servicesgroup.ai.name": {
"message": "人工智慧"
},
"servicesgroup.cdn.name": {
"message": "內容傳遞網路CDN"
},
"servicesgroup.dating.name": {
"message": "約會服務"
},
"servicesgroup.gambling.name": {
"message": "博弈與賭博"
},
"servicesgroup.gaming.name": {
"message": "遊戲"
},
"servicesgroup.hosting.name": {
"message": "Web 主機服務"
},
"servicesgroup.messenger.name": {
"message": "訊息服務"
},
"servicesgroup.privacy.name": {
"message": "隱私工具"
},
"servicesgroup.shopping.name": {
"message": "購物"
},
"servicesgroup.social_network.name": {
"message": "社群網路"
},
"servicesgroup.software.name": {
"message": "軟體開發"
},
"servicesgroup.streaming.name": {
"message": "串流服務"
}
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Trans } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import { Controller, useForm } from 'react-hook-form';
@@ -10,8 +10,13 @@ export type BlockedService = {
id: string;
name: string;
icon_svg: string;
group_id: string;
};
export type ServiceGroups = {
id: string;
}
type FormValues = {
blocked_services: Record<string, boolean>;
};
@@ -19,12 +24,22 @@ type FormValues = {
interface FormProps {
initialValues: Record<string, boolean>;
blockedServices: BlockedService[];
serviceGroups: ServiceGroups[];
onSubmit: (values: FormValues) => void;
processing: boolean;
processingSet: boolean;
}
export const Form = ({ initialValues, blockedServices, processing, processingSet, onSubmit }: FormProps) => {
export const Form = ({
initialValues,
blockedServices,
serviceGroups,
processing,
processingSet,
onSubmit,
}: FormProps) => {
const { t } = useTranslation();
const {
handleSubmit,
control,
@@ -32,58 +47,134 @@ export const Form = ({ initialValues, blockedServices, processing, processingSet
formState: { isSubmitting },
} = useForm<FormValues>({
mode: 'onBlur',
defaultValues: initialValues,
defaultValues: { blocked_services: initialValues }
});
const handleToggleAllServices = async (isSelected: boolean) => {
blockedServices.forEach((service: BlockedService) => setValue(`blocked_services.${service.id}`, isSelected));
const isServicesControlsDisabled = processing || processingSet;
const isSubmitDisabled = processing || processingSet || isSubmitting;
const servicesByGroup = useMemo(() => {
return blockedServices.reduce((acc, service) => {
if (!acc[service.group_id]) {
acc[service.group_id] = [];
}
acc[service.group_id].push(service);
return acc;
}, {} as Record<string, BlockedService[]>);
}, [blockedServices]);
const handleToggleAllServices = (isSelected: boolean) => {
blockedServices.forEach((service) => {
if (!isServicesControlsDisabled) {
setValue(`blocked_services.${service.id}`, isSelected);
}
});
};
const handleToggleGroupServices = (groupId: string, isSelected: boolean) => {
if (isServicesControlsDisabled) {
return;
}
servicesByGroup[groupId].forEach((service) => {
setValue(`blocked_services.${service.id}`, isSelected);
});
};
const handleSubmitWithGroups = (values: FormValues) => {
if (!values || !values.blocked_services) {
return onSubmit(values);
}
const enabledIdsMap = Object.fromEntries(
blockedServices
.filter(service => values.blocked_services?.[service.id])
.map(service => [service.id, true] as const)
);
return onSubmit({ blocked_services: enabledIdsMap });
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit(handleSubmitWithGroups)}>
<div className="form__group">
<div className="row mb-4">
<div className="col-6">
<div className="blocked_services row mb-5">
<div className="col-12 col-md-6 mb-4 mb-md-0">
<button
type="button"
data-testid="blocked_services_block_all"
className="btn btn-secondary btn-block"
disabled={processing || processingSet}
className="btn btn-secondary btn-block font-weight-normal"
disabled={isServicesControlsDisabled}
onClick={() => handleToggleAllServices(true)}>
<Trans>block_all</Trans>
</button>
</div>
<div className="col-6">
<div className="col-12 col-md-6">
<button
type="button"
data-testid="blocked_services_unblock_all"
className="btn btn-secondary btn-block"
disabled={processing || processingSet}
className="btn btn-secondary btn-block font-weight-normal"
disabled={isServicesControlsDisabled}
onClick={() => handleToggleAllServices(false)}>
<Trans>unblock_all</Trans>
</button>
</div>
</div>
<div className="services">
{blockedServices.map((service: BlockedService) => (
<Controller
key={service.id}
name={`blocked_services.${service.id}`}
control={control}
render={({ field }) => (
<ServiceField
{...field}
data-testid={`blocked_services_${service.id}`}
placeholder={service.name}
disabled={processing || processingSet}
icon={service.icon_svg}
/>
{serviceGroups.map((group) => {
const groupServices = servicesByGroup[group.id];
return (
<div key={group.id} className="services-group mb-2">
<h3 className="h5 mb-3">
{t(`servicesgroup.${group.id}.name`, { ns: 'services' })}
</h3>
{groupServices.length > 1 && (
<div className="actions mb-3 d-flex gap-4">
<button
type="button"
className="btn btn-link p-0 text-danger font-weight-normal mr-5"
disabled={isServicesControlsDisabled}
onClick={() => handleToggleGroupServices(group.id, true)}
>
<Trans>block_all</Trans>
</button>
<button
type="button"
className="btn btn-link p-0 text-success font-weight-normal"
disabled={isServicesControlsDisabled}
onClick={() => handleToggleGroupServices(group.id, false)}
>
<Trans>unblock_all</Trans>
</button>
</div>
)}
/>
))}
</div>
<div className="services__wrapper">
<div className="services">
{groupServices.map((service) => (
<Controller
key={service.id}
name={`blocked_services.${service.id}`}
control={control}
render={({ field }) => (
<ServiceField
{...field}
data-testid={`blocked_services_${service.id}`}
data-groupid={`blocked_services_${service.group_id}`}
placeholder={service.name}
disabled={isServicesControlsDisabled}
icon={service.icon_svg}
/>
)}
/>
))}
</div>
</div>
</div>
);
})}
</div>
<div className="btn-list">
@@ -91,7 +182,7 @@ export const Form = ({ initialValues, blockedServices, processing, processingSet
type="submit"
data-testid="blocked_services_save"
className="btn btn-success btn-standard btn-large"
disabled={isSubmitting || processing || processingSet}>
disabled={isSubmitDisabled}>
<Trans>save_btn</Trans>
</button>
</div>

View File

@@ -16,11 +16,11 @@ import { RootState } from '../../../initialState';
const getInitialDataForServices = (initial: any) =>
initial
? initial.reduce(
(acc: any, service: any) => {
acc.blocked_services[service] = true;
(acc: Record<string, boolean>, service: any) => {
acc[service] = true;
return acc;
},
{ blocked_services: {} },
{} as Record<string, boolean>,
)
: initial;
@@ -76,6 +76,7 @@ const Services = () => {
<Form
initialValues={initialValues}
blockedServices={services.allServices}
serviceGroups={services.allGroups}
processing={services.processing}
processingSet={services.processingSet}
onSubmit={handleSubmit}

View File

@@ -1,9 +1,11 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import langDetect from 'i18next-browser-languagedetector';
import { setHtmlLangAttr } from './helpers/helpers';
import { LANGUAGES, BASE_LOCALE } from './helpers/twosky';
// Main translations
import ar from './__locales/ar.json';
import be from './__locales/be.json';
import bg from './__locales/bg.json';
@@ -41,51 +43,218 @@ import zhCN from './__locales/zh-cn.json';
import zhHK from './__locales/zh-hk.json';
import zhTW from './__locales/zh-tw.json';
import { setHtmlLangAttr } from './helpers/helpers';
// Services translations
import arServices from './__locales-services/ar.json';
import beServices from './__locales-services/be.json';
import bgServices from './__locales-services/bg.json';
import csServices from './__locales-services/cs.json';
import daServices from './__locales-services/da.json';
import deServices from './__locales-services/de.json';
import enServices from './__locales-services/en.json';
import esServices from './__locales-services/es.json';
import faServices from './__locales-services/fa.json';
import fiServices from './__locales-services/fi.json';
import frServices from './__locales-services/fr.json';
import hrServices from './__locales-services/hr.json';
import huServices from './__locales-services/hu.json';
import idServices from './__locales-services/id.json';
import itServices from './__locales-services/it.json';
import jaServices from './__locales-services/ja.json';
import koServices from './__locales-services/ko.json';
import nlServices from './__locales-services/nl.json';
import noServices from './__locales-services/no.json';
import plServices from './__locales-services/pl.json';
import ptBRServices from './__locales-services/pt-br.json';
import ptPTServices from './__locales-services/pt-pt.json';
import roServices from './__locales-services/ro.json';
import ruServices from './__locales-services/ru.json';
import siLkServices from './__locales-services/si-lk.json';
import skServices from './__locales-services/sk.json';
import slServices from './__locales-services/sl.json';
import srCSServices from './__locales-services/sr-cs.json';
import svServices from './__locales-services/sv.json';
import thServices from './__locales-services/th.json';
import trServices from './__locales-services/tr.json';
import ukServices from './__locales-services/uk.json';
import viServices from './__locales-services/vi.json';
import zhCNServices from './__locales-services/zh-cn.json';
import zhHKServices from './__locales-services/zh-hk.json';
import zhTWServices from './__locales-services/zh-tw.json';
/**
* Helper function to convert services object into a flat `{ key: message }` format.
*
* Supported formats:
* - { message: "..." }
*
* Example:
* Input: { a: { message: "one" }, b: { message: "two" } }
* Output: { a: "one", b: "two" }
*/
const convertServicesFormat = (
services: Record<string, { message: string }>,
): Record<string, string> => {
return Object.fromEntries(
Object.entries(services).map(([key, value]) => [key, value.message])
);
};
// Resources
const resources = {
ar: { translation: ar },
be: { translation: be },
bg: { translation: bg },
cs: { translation: cs },
da: { translation: da },
de: { translation: de },
en: { translation: en },
'en-us': { translation: en },
es: { translation: es },
fa: { translation: fa },
fi: { translation: fi },
fr: { translation: fr },
hr: { translation: hr },
hu: { translation: hu },
id: { translation: id },
it: { translation: it },
ja: { translation: ja },
ko: { translation: ko },
nl: { translation: nl },
no: { translation: no },
pl: { translation: pl },
'pt-br': { translation: ptBR },
'pt-pt': { translation: ptPT },
ro: { translation: ro },
ru: { translation: ru },
'si-lk': { translation: siLk },
sk: { translation: sk },
sl: { translation: sl },
'sr-cs': { translation: srCS },
sv: { translation: sv },
th: { translation: th },
tr: { translation: tr },
uk: { translation: uk },
vi: { translation: vi },
'zh-cn': { translation: zhCN },
'zh-hk': { translation: zhHK },
'zh-tw': { translation: zhTW },
ar: {
translation: ar,
services: convertServicesFormat(arServices)
},
be: {
translation: be,
services: convertServicesFormat(beServices)
},
bg: {
translation: bg,
services: convertServicesFormat(bgServices)
},
cs: {
translation: cs,
services: convertServicesFormat(csServices)
},
da: {
translation: da,
services: convertServicesFormat(daServices)
},
de: {
translation: de,
services: convertServicesFormat(deServices)
},
en: {
translation: en,
services: convertServicesFormat(enServices)
},
'en-us': {
translation: en,
services: convertServicesFormat(enServices)
},
es: {
translation: es,
services: convertServicesFormat(esServices)
},
fa: {
translation: fa,
services: convertServicesFormat(faServices)
},
fi: {
translation: fi,
services: convertServicesFormat(fiServices)
},
fr: {
translation: fr,
services: convertServicesFormat(frServices)
},
hr: {
translation: hr,
services: convertServicesFormat(hrServices)
},
hu: {
translation: hu,
services: convertServicesFormat(huServices)
},
id: {
translation: id,
services: convertServicesFormat(idServices)
},
it: {
translation: it,
services: convertServicesFormat(itServices)
},
ja: {
translation: ja,
services: convertServicesFormat(jaServices)
},
ko: {
translation: ko,
services: convertServicesFormat(koServices)
},
nl: {
translation: nl,
services: convertServicesFormat(nlServices)
},
no: {
translation: no,
services: convertServicesFormat(noServices)
},
pl: {
translation: pl,
services: convertServicesFormat(plServices)
},
'pt-br': {
translation: ptBR,
services: convertServicesFormat(ptBRServices)
},
'pt-pt': {
translation: ptPT,
services: convertServicesFormat(ptPTServices)
},
ro: {
translation: ro,
services: convertServicesFormat(roServices)
},
ru: {
translation: ru,
services: convertServicesFormat(ruServices)
},
'si-lk': {
translation: siLk,
services: convertServicesFormat(siLkServices)
},
sk: {
translation: sk,
services: convertServicesFormat(skServices)
},
sl: {
translation: sl,
services: convertServicesFormat(slServices)
},
'sr-cs': {
translation: srCS,
services: convertServicesFormat(srCSServices)
},
sv: {
translation: sv,
services: convertServicesFormat(svServices)
},
th: {
translation: th,
services: convertServicesFormat(thServices)
},
tr: {
translation: tr,
services: convertServicesFormat(trServices)
},
uk: {
translation: uk,
services: convertServicesFormat(ukServices)
},
vi: {
translation: vi,
services: convertServicesFormat(viServices)
},
'zh-cn': {
translation: zhCN,
services: convertServicesFormat(zhCNServices)
},
'zh-hk': {
translation: zhHK,
services: convertServicesFormat(zhHKServices)
},
'zh-tw': {
translation: zhTW,
services: convertServicesFormat(zhTWServices)
},
};
const availableLanguages = Object.keys(LANGUAGES);
i18n.use(langDetect)
i18n
.use(langDetect)
.use(initReactI18next)
.init(
{
@@ -95,11 +264,14 @@ i18n.use(langDetect)
keySeparator: false,
nsSeparator: false,
returnEmptyString: false,
ns: ['translation', 'services'],
defaultNS: 'translation',
interpolation: {
escapeValue: false,
},
react: {
wait: true,
bindI18n: 'languageChanged loaded',
},
whitelist: availableLanguages,
},
@@ -108,7 +280,11 @@ i18n.use(langDetect)
i18n.changeLanguage(BASE_LOCALE);
}
setHtmlLangAttr(i18n.language);
},
}
);
i18n.on('languageChanged', (lng) => {
setHtmlLangAttr(lng);
});
export default i18n;

View File

@@ -380,6 +380,7 @@ export type ServicesData = {
processingSet: boolean;
list: any;
allServices: any[];
allGroups: any[];
};
export type RootState = {
@@ -592,6 +593,7 @@ export const initialState: RootState = {
processingSet: false,
list: {},
allServices: [],
allGroups: [],
},
settings: {
processing: true,

View File

@@ -29,6 +29,7 @@ const services = handleActions(
[actions.getAllBlockedServicesSuccess.toString()]: (state, { payload }: any) => ({
...state,
allServices: payload.blocked_services,
allGroups: payload.groups,
processingAll: false,
}),
@@ -51,6 +52,7 @@ const services = handleActions(
processingSet: false,
list: {},
allServices: [],
allGroups: [],
},
);

View File

@@ -206,7 +206,7 @@ Optional environment:
- `go run ./scripts/translations help`: print usage.
- `go run ./scripts/translations download [-n <count>]`: download and save all translations. `n` is optional flag where count is a number of concurrent downloads.
- `go run ./scripts/translations download [-n <count>]`: download and save all translations. `n` is optional flag where count is a number of concurrent downloads. Note, that it downloads locales for all configurations in the `.twosky.json` file.
- `go run ./scripts/translations upload`: upload the base `en` locale.
@@ -228,6 +228,8 @@ Optional environment:
- `TWOSKY_PROJECT_ID`: set an alternative project ID for `download` or `upload`.
Deprectated: This environment variable should not be used since the script began supporting multiple configurations.
## `companiesdb/`: Whotracks.me database converter
A simple script that downloads and updates the companies DB in the `client` code from [the repo][companiesrepo].

View File

@@ -17,33 +17,22 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/ioutil"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/syncutil"
"github.com/AdguardTeam/golibs/validate"
)
// download and save all translations.
func (c *twoskyClient) download(ctx context.Context, l *slog.Logger) (err error) {
var numWorker int
flagSet := flag.NewFlagSet("download", flag.ExitOnError)
flagSet.Usage = func() {
usage("download command error")
}
flagSet.IntVar(&numWorker, "n", 1, "number of concurrent downloads")
err = flagSet.Parse(os.Args[2:])
func (c *twoskyClient) download(ctx context.Context, l *slog.Logger) {
numWorker, err := parseDownloadArgs()
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
if numWorker < 1 {
usage("count must be positive")
usage(err.Error())
}
downloadURI := c.uri.JoinPath("download")
wg := &sync.WaitGroup{}
uriCh := make(chan *url.URL, len(c.langs))
reqCh := make(chan downloadRequest, numWorker)
dw := &downloadWorker{
ctx: ctx,
@@ -52,25 +41,44 @@ func (c *twoskyClient) download(ctx context.Context, l *slog.Logger) (err error)
client: &http.Client{
Timeout: 10 * time.Second,
},
uriCh: uriCh,
reqCh: reqCh,
}
for range numWorker {
wg.Go(dw.run)
}
for _, lang := range c.langs {
uri := translationURL(downloadURI, defaultBaseFile, c.projectID, lang)
for _, baseFile := range c.localizableFiles {
dir, file := filepath.Split(baseFile)
uriCh <- uri
for _, lang := range c.langs {
uri := translationURL(downloadURI, file, c.projectID, lang)
reqCh <- downloadRequest{
uri: uri,
dir: dir,
}
}
}
close(uriCh)
close(reqCh)
wg.Wait()
printFailedLocales(ctx, l, dw.failed)
}
return nil
// parseDownloadArgs parses command-line arguments for the download command.
func parseDownloadArgs() (numWorker int, err error) {
flagSet := flag.NewFlagSet("download", flag.ExitOnError)
flagSet.IntVar(&numWorker, "n", 1, "number of concurrent downloads")
err = flagSet.Parse(os.Args[2:])
if err != nil {
// Don't wrap the error since it's informative enough as is.
return 0, err
}
return numWorker, validate.Positive("count", numWorker)
}
// printFailedLocales prints sorted list of failed downloads, if any. l and
@@ -102,17 +110,24 @@ type downloadWorker struct {
l *slog.Logger
failed *syncutil.Map[string, struct{}]
client *http.Client
uriCh <-chan *url.URL
reqCh <-chan downloadRequest
}
// downloadRequest is a request to download a translation. All fields must not
// be empty.
type downloadRequest struct {
uri *url.URL
dir string
}
// run handles the channel of URLs, one by one. It returns when the channel is
// closed. It's used to be run in a separate goroutine.
func (w *downloadWorker) run() {
for uri := range w.uriCh {
q := uri.Query()
for req := range w.reqCh {
q := req.uri.Query()
code := q.Get("language")
err := saveToFile(w.ctx, w.l, w.client, uri, code)
err := saveToFile(w.ctx, w.l, w.client, req.uri, code, req.dir)
if err != nil {
w.l.ErrorContext(w.ctx, "download worker", slogutil.KeyError, err)
w.failed.Store(code, struct{}{})
@@ -128,12 +143,17 @@ func saveToFile(
client *http.Client,
uri *url.URL,
code string,
localesDir string,
) (err error) {
data, err := getTranslation(ctx, l, client, uri.String())
if err != nil {
return fmt.Errorf("getting translation %q: %s", code, err)
}
if data[len(data)-1] != '\n' {
data = append(data, '\n')
}
name := filepath.Join(localesDir, code+".json")
err = os.WriteFile(name, data, 0o664)
if err != nil {
@@ -146,7 +166,8 @@ func saveToFile(
}
// getTranslation returns received translation data and error. If err is not
// nil, data may contain a response from server for inspection.
// nil, data may contain a response from server for inspection. Otherwise, the
// data is guaranteed to be non-empty.
func getTranslation(
ctx context.Context,
l *slog.Logger,
@@ -166,17 +187,19 @@ func getTranslation(
// Go on and download the body for inspection.
}
limitReader := ioutil.LimitReader(resp.Body, readLimit)
limitReader := ioutil.LimitReader(resp.Body, readLimit.Bytes())
data, readErr := io.ReadAll(limitReader)
if readErr != nil {
return nil, errors.WithDeferred(err, readErr)
}
return data, errors.WithDeferred(err, readErr)
return data, validate.NotEmptySlice("response", data)
}
// translationURL returns a new url.URL with provided query parameters.
func translationURL(oldURL *url.URL, baseFile, projectID string, lang langCode) (uri *url.URL) {
uri = &url.URL{}
*uri = *oldURL
func translationURL(baseURL *url.URL, baseFile, projectID string, lang langCode) (uri *url.URL) {
uri = netutil.CloneURL(baseURL)
q := uri.Query()
q.Set("format", "json")

View File

@@ -5,13 +5,11 @@ package main
import (
"bufio"
"bytes"
"cmp"
"context"
"encoding/json"
"fmt"
"log/slog"
"maps"
"net/url"
"os"
"path/filepath"
"slices"
@@ -23,17 +21,18 @@ import (
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/osutil"
"github.com/AdguardTeam/golibs/osutil/executil"
"github.com/c2h5oh/datasize"
)
// TODO(e.burkov): Remove the default as they should be set by configuration.
const (
twoskyConfFile = "./.twosky.json"
localesDir = "./client/src/__locales"
defaultBaseFile = "en.json"
defaultProjectID = "home"
srcDir = "./client/src"
twoskyURI = "https://twosky.int.agrd.dev/api/v1"
twoskyConfFile = "./.twosky.json"
localesDirHome = "./client/src/__locales"
defaultBaseFile = "en.json"
srcDir = "./client/src"
twoskyURI = "https://twosky.int.agrd.dev/api/v1"
readLimit = 1 * 1024 * 1024
readLimit = 1 * datasize.MB
uploadTimeout = 20 * time.Second
)
@@ -77,27 +76,27 @@ func main() {
usage("")
}
conf := errors.Must(readTwoskyConfig())
homeConf, servicesConf, err := readTwoskyConfig()
errors.Check(err)
var cli *twoskyClient
switch os.Args[1] {
case "summary":
errors.Check(summary(conf.Languages))
errors.Check(summary(homeConf.Languages))
case "download":
cli = errors.Must(conf.toClient())
cli = errors.Must(newTwoskyClient(homeConf))
cli.download(ctx, l)
errors.Check(cli.download(ctx, l))
cli = errors.Must(newTwoskyClient(servicesConf))
cli.download(ctx, l)
case "unused":
err := unused(ctx, l, conf.LocalizableFiles[0])
errors.Check(err)
errors.Check(unused(ctx, l, homeConf.LocalizableFiles[0]))
case "upload":
cli = errors.Must(conf.toClient())
cli = errors.Must(newTwoskyClient(homeConf))
errors.Check(cli.upload())
case "auto-add":
err := autoAdd(ctx, l, conf.LocalizableFiles[0])
errors.Check(err)
errors.Check(autoAdd(ctx, l, homeConf.LocalizableFiles[0]))
default:
usage("unknown command")
}
@@ -133,107 +132,6 @@ Commands:
os.Exit(osutil.ExitCodeSuccess)
}
// twoskyConfig is the configuration structure for localization.
type twoskyConfig struct {
Languages languages `json:"languages"`
ProjectID string `json:"project_id"`
BaseLangcode langCode `json:"base_locale"`
LocalizableFiles []string `json:"localizable_files"`
}
// readTwoskyConfig returns twosky configuration.
func readTwoskyConfig() (t *twoskyConfig, err error) {
defer func() { err = errors.Annotate(err, "parsing twosky config: %w") }()
b, err := os.ReadFile(twoskyConfFile)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
var tsc []twoskyConfig
err = json.Unmarshal(b, &tsc)
if err != nil {
return nil, fmt.Errorf("unmarshalling %q: %w", twoskyConfFile, err)
}
if len(tsc) == 0 {
return nil, fmt.Errorf("%q is empty", twoskyConfFile)
}
conf := tsc[0]
for _, lang := range conf.Languages {
if lang == "" {
return nil, errors.Error("language is empty")
}
}
if len(conf.LocalizableFiles) == 0 {
return nil, errors.Error("no localizable files specified")
}
return &conf, nil
}
// twoskyClient is the twosky client with methods for download and upload
// translations.
type twoskyClient struct {
// uri is the base URL.
uri *url.URL
// projectID is the name of the project.
projectID string
// baseLang is the base language code.
baseLang langCode
// langs is the list of codes of languages to download.
langs []langCode
}
// toClient reads values from environment variables or defaults, validates
// them, and returns the twosky client.
func (t *twoskyConfig) toClient() (cli *twoskyClient, err error) {
defer func() { err = errors.Annotate(err, "filling config: %w") }()
uriStr := cmp.Or(os.Getenv("TWOSKY_URI"), twoskyURI)
uri, err := url.Parse(uriStr)
if err != nil {
return nil, err
}
projectID := cmp.Or(os.Getenv("TWOSKY_PROJECT_ID"), defaultProjectID)
baseLang := t.BaseLangcode
uLangStr := os.Getenv("UPLOAD_LANGUAGE")
if uLangStr != "" {
baseLang = langCode(uLangStr)
}
langs := slices.Sorted(maps.Keys(t.Languages))
dlLangStr := os.Getenv("DOWNLOAD_LANGUAGES")
if dlLangStr == "blocker" {
langs = blockerLangCodes
} else if dlLangStr != "" {
var dlLangs []langCode
dlLangs, err = validateLanguageStr(dlLangStr, t.Languages)
if err != nil {
return nil, err
}
langs = dlLangs
}
return &twoskyClient{
uri: uri,
projectID: projectID,
baseLang: baseLang,
langs: langs,
}, nil
}
// validateLanguageStr validates languages codes that contain in the str and
// returns them or error.
func validateLanguageStr(str string, all languages) (langs []langCode, err error) {
@@ -274,8 +172,11 @@ func readLocales(fn string) (loc locales, err error) {
}
// summary prints summary for translations.
//
// TODO(e.burkov): Consider making it a method of [twoskyClient] and
// calculating summary for all configurations.
func summary(langs languages) (err error) {
basePath := filepath.Join(localesDir, defaultBaseFile)
basePath := filepath.Join(localesDirHome, defaultBaseFile)
baseLoc, err := readLocales(basePath)
if err != nil {
return fmt.Errorf("summary: %w", err)
@@ -283,10 +184,8 @@ func summary(langs languages) (err error) {
size := float64(len(baseLoc))
keys := slices.Sorted(maps.Keys(langs))
for _, lang := range keys {
name := filepath.Join(localesDir, string(lang)+".json")
for _, lang := range slices.Sorted(maps.Keys(langs)) {
name := filepath.Join(localesDirHome, string(lang)+".json")
if name == basePath {
continue
}
@@ -314,6 +213,9 @@ func summary(langs languages) (err error) {
}
// unused prints unused text labels.
//
// TODO(e.burkov): Consider making it a method of [twoskyClient] and searching
// unused strings for all configurations.
func unused(ctx context.Context, l *slog.Logger, basePath string) (err error) {
defer func() { err = errors.Annotate(err, "unused: %w") }()
@@ -322,7 +224,7 @@ func unused(ctx context.Context, l *slog.Logger, basePath string) (err error) {
return err
}
locDir := filepath.Clean(localesDir)
locDir := filepath.Clean(localesDirHome)
js, err := findJS(ctx, l, locDir)
if err != nil {
return err
@@ -484,7 +386,7 @@ func changedLocales(
) (adds, dels []string, err error) {
defer func() { err = errors.Annotate(err, "getting changes: %w") }()
gitArgs := []string{"diff", "--numstat", localesDir}
gitArgs := []string{"diff", "--numstat", localesDirHome}
l.DebugContext(ctx, "executing", "cmd", gitCmd, "args", gitArgs)
// TODO(s.chzhen): Consider streaming the output if needed. Using

View File

@@ -0,0 +1,165 @@
package main
import (
"cmp"
"encoding/json"
"fmt"
"maps"
"net/url"
"os"
"slices"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/validate"
)
// Constants for mapping the twosky configurations.
//
// Keep in sync with the .twosky.json file.
const (
// twoskyProjectIdxHome is the index of the Home project in the localization
// configuration.
twoskyProjectIdxHome = iota
// twoskyProjectIdxServices is the index of the Services project in the
// localization configuration.
twoskyProjectIdxServices
// twoskyProjectCount is the number of projects in the localization
// configuration.
twoskyProjectCount
)
// twoskyConfig is the configuration structure for localization of a single
// project.
type twoskyConfig struct {
Languages languages `json:"languages"`
ProjectID string `json:"project_id"`
BaseLangcode langCode `json:"base_locale"`
LocalizableFiles []string `json:"localizable_files"`
}
// type check
var _ validate.Interface = (*twoskyConfig)(nil)
// Validate implements the [validate.Interface] interface for *twoskyConfig.
func (t *twoskyConfig) Validate() (err error) {
if t == nil {
return errors.ErrNoValue
}
errs := []error{
validate.NotEmpty("project_id", t.ProjectID),
validate.NotEmpty("base_locale", t.BaseLangcode),
validate.NotEmptySlice("localizable_files", t.LocalizableFiles),
}
if len(t.Languages) == 0 {
errs = append(errs, fmt.Errorf("languages: %w", errors.ErrEmptyValue))
}
for code, lang := range t.Languages {
err = validate.NotEmpty("languages: "+string(code), lang)
if err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
// readTwoskyConfig returns twosky configuration.
func readTwoskyConfig() (home, services *twoskyConfig, err error) {
defer func() { err = errors.Annotate(err, "parsing twosky config: %w") }()
b, err := os.ReadFile(twoskyConfFile)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, nil, err
}
var tsc []*twoskyConfig
err = json.Unmarshal(b, &tsc)
if err != nil {
return nil, nil, fmt.Errorf("unmarshalling %q: %w", twoskyConfFile, err)
}
err = validate.Equal("projects count", len(tsc), twoskyProjectCount)
if err != nil {
return nil, nil, err
}
err = errors.Join(validate.AppendSlice(nil, "projects", tsc)...)
if err != nil {
return nil, nil, err
}
return tsc[twoskyProjectIdxHome], tsc[twoskyProjectIdxServices], nil
}
// twoskyClient is the twosky client with methods for download and upload
// translations.
type twoskyClient struct {
// uri is the base URL.
uri *url.URL
// projectID is the name of the project.
projectID string
// baseLang is the base language code.
baseLang langCode
// langs is the list of codes of languages to download.
langs []langCode
// localizableFiles are the files to localize.
localizableFiles []string
}
// newTwoskyClient reads values from environment variables or defaults,
// validates them, and returns the twosky client.
func newTwoskyClient(conf *twoskyConfig) (cli *twoskyClient, err error) {
defer func() { err = errors.Annotate(err, "filling config: %w") }()
uriStr := cmp.Or(os.Getenv("TWOSKY_URI"), twoskyURI)
uri, err := url.Parse(uriStr)
if err != nil {
return nil, err
}
// TODO(e.burkov): Don't use env.
projectID := conf.ProjectID
envProjectID := os.Getenv("PROJECT_ID")
if envProjectID != "" {
projectID = envProjectID
}
baseLang := conf.BaseLangcode
uLangStr := os.Getenv("UPLOAD_LANGUAGE")
if uLangStr != "" {
baseLang = langCode(uLangStr)
}
langs := slices.Sorted(maps.Keys(conf.Languages))
dlLangStr := os.Getenv("DOWNLOAD_LANGUAGES")
if dlLangStr == "blocker" {
langs = blockerLangCodes
} else if dlLangStr != "" {
var dlLangs []langCode
dlLangs, err = validateLanguageStr(dlLangStr, conf.Languages)
if err != nil {
return nil, err
}
langs = dlLangs
}
return &twoskyClient{
uri: uri,
projectID: projectID,
baseLang: baseLang,
langs: langs,
localizableFiles: conf.LocalizableFiles,
}, nil
}

View File

@@ -17,12 +17,12 @@ import (
"github.com/AdguardTeam/golibs/httphdr"
)
// upload base translation.
// upload uploads the base locale file.
func (c *twoskyClient) upload() (err error) {
defer func() { err = errors.Annotate(err, "upload: %w") }()
uploadURI := c.uri.JoinPath("upload")
basePath := filepath.Join(localesDir, defaultBaseFile)
basePath := filepath.Join(localesDirHome, defaultBaseFile)
formData := map[string]string{
"format": "json",
@@ -64,10 +64,7 @@ func prepareMultipartMsg(
if err != nil {
return nil, "", fmt.Errorf("opening file: %w", err)
}
defer func() {
err = errors.WithDeferred(err, file.Close())
}()
defer func() { err = errors.WithDeferred(err, file.Close()) }()
h := make(textproto.MIMEHeader)
h.Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON)