mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2025-12-20 01:11:03 +08:00
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: c70212ec7ff0ef4f39Author: 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 commit1a865f0856Author: 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 commitc2e1b8edeeAuthor: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Mon Nov 10 18:03:23 2025 +0300 all: imp translations script commitc930dcff36Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Mon Nov 10 17:52:44 2025 +0300 scripts: revert master commitc28e13b4e1Author: Elizaveta <e.egorova@adguard.com> Date: Wed Nov 5 16:43:13 2025 +0300 move project key const commitee977f5993Author: Elizaveta <e.egorova@adguard.com> Date: Wed Nov 5 16:25:34 2025 +0300 update download script commit244c1011b0Author: Elizaveta <e.egorova@adguard.com> Date: Sat Nov 1 15:59:04 2025 +0300 restore master toggle commit40418ea8cfAuthor: Elizaveta <e.egorova@adguard.com> Date: Sat Nov 1 14:59:57 2025 +0300 update saveToFile func commite4f3c677d4Author: Elizaveta <e.egorova@adguard.com> Date: Sat Nov 1 14:52:14 2025 +0300 go linter commit5359590953Author: Elizaveta <e.egorova@adguard.com> Date: Sat Nov 1 14:44:39 2025 +0300 remove en group tr from __locales commita91215dfadAuthor: Elizaveta <e.egorova@adguard.com> Date: Sat Nov 1 14:43:39 2025 +0300 add translations commitc65f80048dAuthor: Elizaveta <e.egorova@adguard.com> Date: Sat Nov 1 12:15:07 2025 +0300 fix ui commit893433bd8fAuthor: Elizaveta <e.egorova@adguard.com> Date: Fri Oct 31 17:22:44 2025 +0300 fix comment commit26148996c9Author: Elizaveta <e.egorova@adguard.com> Date: Fri Oct 31 16:49:46 2025 +0300 styles commit2785958252Author: Elizaveta <e.egorova@adguard.com> Date: Fri Oct 31 16:48:46 2025 +0300 linter ... and 6 more commits
This commit is contained in:
45
.twosky.json
45
.twosky.json
@@ -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": "正體中文(台灣)"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -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.
|
||||
-->
|
||||
|
||||
38
client/src/__locales-services/ar.json
Normal file
38
client/src/__locales-services/ar.json
Normal 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": "خدمات البث المباشر"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/be.json
Normal file
38
client/src/__locales-services/be.json
Normal 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": "Стрымінгавыя сэрвісы"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/bg.json
Normal file
38
client/src/__locales-services/bg.json
Normal 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": "Стрийминг услуги"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/cs.json
Normal file
38
client/src/__locales-services/cs.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/da.json
Normal file
38
client/src/__locales-services/da.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/de.json
Normal file
38
client/src/__locales-services/de.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/en.json
Normal file
38
client/src/__locales-services/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/es.json
Normal file
38
client/src/__locales-services/es.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/fa.json
Normal file
38
client/src/__locales-services/fa.json
Normal 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": "خدمات استریم"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/fi.json
Normal file
38
client/src/__locales-services/fi.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/fr.json
Normal file
38
client/src/__locales-services/fr.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/hr.json
Normal file
38
client/src/__locales-services/hr.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/hu.json
Normal file
38
client/src/__locales-services/hu.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/id.json
Normal file
38
client/src/__locales-services/id.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/it.json
Normal file
38
client/src/__locales-services/it.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/ja.json
Normal file
38
client/src/__locales-services/ja.json
Normal 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": "ストリーミングサービス"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/ko.json
Normal file
38
client/src/__locales-services/ko.json
Normal 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": "스트리밍 서비스"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/nl.json
Normal file
38
client/src/__locales-services/nl.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/no.json
Normal file
38
client/src/__locales-services/no.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/pl.json
Normal file
38
client/src/__locales-services/pl.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/pt-br.json
Normal file
38
client/src/__locales-services/pt-br.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/pt-pt.json
Normal file
38
client/src/__locales-services/pt-pt.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/ro.json
Normal file
38
client/src/__locales-services/ro.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/ru.json
Normal file
38
client/src/__locales-services/ru.json
Normal 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": "Стриминговые сервисы"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/si-lk.json
Normal file
38
client/src/__locales-services/si-lk.json
Normal 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": "ප්රවාහ සේවා"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/sk.json
Normal file
38
client/src/__locales-services/sk.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/sl.json
Normal file
38
client/src/__locales-services/sl.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/sr-cs.json
Normal file
38
client/src/__locales-services/sr-cs.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/sv.json
Normal file
38
client/src/__locales-services/sv.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/th.json
Normal file
38
client/src/__locales-services/th.json
Normal 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": "บริการสตรีมมิ่ง"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/tr.json
Normal file
38
client/src/__locales-services/tr.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/uk.json
Normal file
38
client/src/__locales-services/uk.json
Normal 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": "Стримінги"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/vi.json
Normal file
38
client/src/__locales-services/vi.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/zh-cn.json
Normal file
38
client/src/__locales-services/zh-cn.json
Normal 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": "串流服务"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/zh-hk.json
Normal file
38
client/src/__locales-services/zh-hk.json
Normal 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": "串流服務"
|
||||
}
|
||||
}
|
||||
38
client/src/__locales-services/zh-tw.json
Normal file
38
client/src/__locales-services/zh-tw.json
Normal 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": "串流服務"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
165
scripts/translations/twosky.go
Normal file
165
scripts/translations/twosky.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user