From 0c5bede1ed80437cd844b6ed8efed7539a6f4c47 Mon Sep 17 00:00:00 2001 From: Melior Date: Thu, 23 Jul 2020 17:53:04 +0200 Subject: [PATCH 01/12] Translated using Weblate (Hindi) Currently translated at 76.6% (416 of 543 strings) Translation: Habitica/Backgrounds Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hi/ Translated using Weblate (Hindi) Currently translated at 98.7% (82 of 83 strings) Translation: Habitica/Achievements Translate-URL: https://translate.habitica.com/projects/habitica/achievements/hi/ Translated using Weblate (Czech) Currently translated at 100.0% (31 of 31 strings) Translation: Habitica/Maintenance Translate-URL: https://translate.habitica.com/projects/habitica/maintenance/cs/ Translated using Weblate (Polish) Currently translated at 81.5% (1727 of 2119 strings) Translation: Habitica/Gear Translate-URL: https://translate.habitica.com/projects/habitica/gear/pl/ Translated using Weblate (Czech) Currently translated at 100.0% (212 of 212 strings) Translation: Habitica/Settings Translate-URL: https://translate.habitica.com/projects/habitica/settings/cs/ Translated using Weblate (Hindi) Currently translated at 76.6% (416 of 543 strings) Translation: Habitica/Backgrounds Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hi/ Translated using Weblate (Hindi) Currently translated at 76.6% (416 of 543 strings) Translation: Habitica/Backgrounds Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hi/ Translated using Weblate (Polish) Currently translated at 100.0% (250 of 250 strings) Translation: Habitica/Subscriber Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pl/ Translated using Weblate (Polish) Currently translated at 81.4% (1726 of 2119 strings) Translation: Habitica/Gear Translate-URL: https://translate.habitica.com/projects/habitica/gear/pl/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (543 of 543 strings) Translation: Habitica/Backgrounds Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/vi/ Translated using Weblate (Vietnamese) Currently translated at 90.7% (127 of 140 strings) Translation: Habitica/Quests Translate-URL: https://translate.habitica.com/projects/habitica/quests/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (124 of 124 strings) Translation: Habitica/Communityguidelines Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/vi/ Translated using Weblate (Vietnamese) Currently translated at 100.0% (212 of 212 strings) Translation: Habitica/Settings Translate-URL: https://translate.habitica.com/projects/habitica/settings/vi/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (124 of 124 strings) Translation: Habitica/Communityguidelines Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt_BR/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (212 of 212 strings) Translation: Habitica/Settings Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt_BR/ Translated using Weblate (Czech) Currently translated at 100.0% (173 of 173 strings) Translation: Habitica/Npc Translate-URL: https://translate.habitica.com/projects/habitica/npc/cs/ Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (124 of 124 strings) Translation: Habitica/Communityguidelines Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/zh_Hant/ Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (124 of 124 strings) Translation: Habitica/Communityguidelines Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/zh_Hans/ Translated using Weblate (German) Currently translated at 100.0% (702 of 702 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/ Translated using Weblate (English (United Kingdom)) Currently translated at 100.0% (124 of 124 strings) Translation: Habitica/Communityguidelines Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/en_GB/ Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (212 of 212 strings) Translation: Habitica/Settings Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hant/ Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (212 of 212 strings) Translation: Habitica/Settings Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hans/ Translated using Weblate (English (United Kingdom)) Currently translated at 100.0% (212 of 212 strings) Translation: Habitica/Settings Translate-URL: https://translate.habitica.com/projects/habitica/settings/en_GB/ Translated using Weblate (Czech) Currently translated at 94.2% (163 of 173 strings) Translation: Habitica/Npc Translate-URL: https://translate.habitica.com/projects/habitica/npc/cs/ Translated using Weblate (Polish) Currently translated at 100.0% (360 of 360 strings) Translation: Habitica/Content Translate-URL: https://translate.habitica.com/projects/habitica/content/pl/ Translated using Weblate (Italian) Currently translated at 100.0% (213 of 213 strings) Translation: Habitica/Tasks Translate-URL: https://translate.habitica.com/projects/habitica/tasks/it/ Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (2119 of 2119 strings) Translation: Habitica/Gear Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/ Translated using Weblate (Japanese) Currently translated at 89.0% (1887 of 2119 strings) Translation: Habitica/Gear Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/ Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (702 of 702 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/ Translated using Weblate (Japanese) Currently translated at 100.0% (124 of 124 strings) Translation: Habitica/Communityguidelines Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ja/ Translated using Weblate (Italian) Currently translated at 100.0% (124 of 124 strings) Translation: Habitica/Communityguidelines Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/it/ Translated using Weblate (Italian) Currently translated at 100.0% (83 of 83 strings) Translation: Habitica/Achievements Translate-URL: https://translate.habitica.com/projects/habitica/achievements/it/ Translated using Weblate (Japanese) Currently translated at 100.0% (212 of 212 strings) Translation: Habitica/Settings Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/ Translated using Weblate (German) Currently translated at 100.0% (124 of 124 strings) Translation: Habitica/Communityguidelines Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/de/ Translated using Weblate (German) Currently translated at 100.0% (212 of 212 strings) Translation: Habitica/Settings Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/ --- website/common/locales/cs/maintenance.json | 63 +++++++++---------- website/common/locales/cs/npc.json | 36 ++++++----- website/common/locales/cs/settings.json | 3 +- .../locales/de/communityguidelines.json | 2 +- website/common/locales/de/questscontent.json | 2 +- website/common/locales/de/settings.json | 3 +- .../locales/en_GB/communityguidelines.json | 2 +- website/common/locales/en_GB/settings.json | 3 +- .../common/locales/hi_IN/achievements.json | 2 +- website/common/locales/hi_IN/backgrounds.json | 6 +- website/common/locales/it/achievements.json | 8 +-- .../locales/it/communityguidelines.json | 2 +- website/common/locales/it/tasks.json | 2 +- .../locales/ja/communityguidelines.json | 2 +- website/common/locales/ja/gear.json | 6 +- website/common/locales/ja/settings.json | 3 +- website/common/locales/pl/content.json | 2 +- website/common/locales/pl/gear.json | 4 +- website/common/locales/pl/subscriber.json | 2 +- .../locales/pt_BR/communityguidelines.json | 2 +- website/common/locales/pt_BR/settings.json | 3 +- website/common/locales/vi/backgrounds.json | 36 +++++------ .../locales/vi/communityguidelines.json | 2 +- website/common/locales/vi/quests.json | 5 +- website/common/locales/vi/settings.json | 3 +- .../locales/zh/communityguidelines.json | 4 +- website/common/locales/zh/gear.json | 2 +- website/common/locales/zh/questscontent.json | 4 +- website/common/locales/zh/settings.json | 3 +- .../locales/zh_TW/communityguidelines.json | 2 +- website/common/locales/zh_TW/settings.json | 3 +- 31 files changed, 121 insertions(+), 101 deletions(-) diff --git a/website/common/locales/cs/maintenance.json b/website/common/locales/cs/maintenance.json index 7bcb71d695..b2b63b263d 100644 --- a/website/common/locales/cs/maintenance.json +++ b/website/common/locales/cs/maintenance.json @@ -1,34 +1,33 @@ { - "habiticaBackSoon": "Nebojte se, Habitica bude brzy zpět!", - "importantMaintenance": "Právě probíhá důležitá údržba. Odhadujeme, že bude trvat do 10PM Pacifického času (5:00AM UTC).", - "maintenance": "Údržba", - "maintenanceMoreInfo": "Chcete více informací ohledně údržby? <%= linkStart %>Podívejte se na naši stránku<%= linkEnd %>.", - "noDamageKeepStreaks": "Neutrpíte ŽÁDNÉ poškození a neztratíte vaše série úspěšnosti!", - "thanksForPatience": "Děkujeme za vaší trpělivost.", - "twitterMaintenanceUpdates": "Pro nejnovější novinky sledujte náš Twitter, kde budeme přidávat více informací.", - "veteranPetAward": "Po údržbě obdržíte Veteránského mazlíčka!", - - "maintenanceInfoTitle": "Informace o nadcházející údržbě země Habitica", - "maintenanceInfoWhat": "Co se děje?", - "maintenanceInfoWhatText": "21. května bude Habitica většinu dne mimo provoz, z důvodu údržby. Během víkendu neutrpíte žádné poškození a to, ani když se včas nepřihlásíte a neodškrtnete si své denní úkoly. Budeme se snažit zprovoznit program Habitica co nejdříve, novinky ohledně údržby budeme přidávat na náš Twitter účet. Po údržbě obdržíte Veteránského mazlíčka!", - "maintenanceInfoWhy": "Proč se to děje?", - "maintenanceInfoWhyText": "Pár posledních měsíců jsme zemi Habitica v pozadí důkladně rekonstruovali. Specificky, jsme přepsali API. I když to na povrchu možná nevypadá o tolik jinak, pod povrchem je to naprosto nový svět. V budoucnosti nám toto povolí vytvořit mnohem snadněji mnoho funkcí a zlepšit celkovou hratelnost!", - "maintenanceInfoTechDetails": "Chcete více detailů ohledně technické stránky procesu? Navštivte Kovárnu, náš blog o vývoji.", - "maintenanceInfoMore": "Více informací", - "maintenanceInfoAccountChanges": "Jaké změn, ve svém účtu, si budu moci všimnout, až bude přepis dokončen? ", - "maintenanceInfoAccountChangesText": "Nejdříve nebudou žádné viditelné změny až na zlepšení výkonu pro funkce jako např. Výzvy. Pokuď si všimneš jakékoli změny, která tam nemá být, napiš nám email na <%= hrefTechAssistanceEmail %> a my se na to podíváme!", - "maintenanceInfoAddFeatures": "Jaké vylepšení se přidají do země Habitica?", - "maintenanceInfoAddFeaturesText": "Dokončení tohoto přepisu nám dovolí začít budovat vylepšený chat a Cechy, plány pro organizace a rodiny a další produktivní funkce, například Měsíčky a možnost si nahrát včerejší aktivitu! To jsou všechno funkce, které jsou jednotlivé, takže bude trvat déle je zavést,pokud bychom však nezačali s tímto přepisem, nebylo by možné je začít provádět.", - "maintenanceInfoHowLong": "Jako dlouho bude údržba trvat?", - "maintenanceInfoHowLongText": "Musíme převést úkoly a data pro 1.3 milióny uživatelů programu Habitica - nejednoduchý úkol! Očekáváme, že se údržba bude konat od (20:00 UTC) do (05:00 UTC). Buďte si jistí, že děláme co je v našich silách, abychom vše stihli co nejrychleji. Můžete sledovat aktualizace na našem Twitteru.", - "maintenanceInfoStatsAffected": "Co se stane s mými Denními úkoly, Sériemi úspěšnosti, Bonusy a Výpravami?", - "maintenanceInfoStatsAffectedText1": "Během víkendu neutrpíte ŽÁDNÉ poškození a neztratíte vaše série úspěšnosti, váš sen se však resetuje normálně! Denní úkoly, které jste odškrtli budou odškrtnuté, bonusy se resetují, atd. Jste-li na sběratelské výpravě, stále budete nalézat předměty, jste-li v bitvě s Bossem, stále budete působit poškození, ovšem Boss nebude působit poškození vám. (I příšery potřebují pauzu!)", - "maintenanceInfoStatsAffectedText2": "Po dlouhém přemýšlení se náš tým rozhodl, že toto je nejférovější cesta jak si poradit se skutečností, že mnoho uživatelů nebude schopno si odškrtnout své Denní úkoly během údržby. Omlouváme se za veškeré nepříjemnosti!", - "maintenanceInfoSeeTasks": "Co když potřebuji vidět svůj list úkolů?", - "maintenanceInfoSeeTasksText": "Budete-li potřebovat vidět váš seznam úkolů v sobotu, abyste věděli co dělat, doporučujeme vám si ho před začátkem údržby vyfotit.", - "maintenanceInfoRarePet": "Jakého vzácného mazlíčka dostanu?", - "maintenanceInfoRarePetText": "Abychom vám poděkovali za vaší trpělivost během prostoje, každý dostane vzácného Veteránského mazlíčka. Pokud jste žádného nikdy předtím nedostali, obdržíte Veteránského vlka. Máte-li už Veteránského vlka, obdržíte Veteránského tygra. Máte-li už oba tyto mazlíčky, obdržíte Veteránského mazlíčka, kterého ještě nikdo nikdy neviděl! Může trvat i několik hodin, po dokončení migrace serveru, než se váš mazlíček objeví ale nebojte se, každý jednoho dostane.", - "maintenanceInfoWho": "Kdo pracoval na tomto obrovském projektu?", - "maintenanceInfoWhoText": "Děkujeme za optání! Bylo to vedeno naším úžasným přispěvatelem jménem paglias, s hodně pomocí od hráčů, kteří si říkají Blade, TheHollidayInn, SabreCat, Victor Pudeyev, TheUnknown, a Alys.", - "maintenanceInfoTesting": "Tato nová verze byla také neúnavně testovaná skupinou úžasných dobrovolníků. Děkujeme vám - bez vás bychom to nezvládli." + "habiticaBackSoon": "Nebojte se, Habitica bude brzy zpět!", + "importantMaintenance": "Právě probíhá důležitá údržba. Odhadujeme, že bude trvat do 10PM Pacifického času (5:00AM UTC).", + "maintenance": "Údržba", + "maintenanceMoreInfo": "Chcete více informací ohledně údržby? <%= linkStart %>Podívejte se na naši stránku<%= linkEnd %>.", + "noDamageKeepStreaks": "Neutrpíte ŽÁDNÉ poškození a neztratíte vaše série úspěšnosti!", + "thanksForPatience": "Děkujeme za vaší trpělivost!", + "twitterMaintenanceUpdates": "Pro nejnovější novinky sledujte náš Twitter, kde budeme přidávat více informací.", + "veteranPetAward": "Po údržbě obdržíte Veteránského mazlíčka!", + "maintenanceInfoTitle": "Informace o nadcházející údržbě země Habitica", + "maintenanceInfoWhat": "Co se děje?", + "maintenanceInfoWhatText": "21. května bude Habitica většinu dne mimo provoz, z důvodu údržby. Během víkendu neutrpíte žádné poškození a to, ani když se včas nepřihlásíte a neodškrtnete si své denní úkoly. Budeme se snažit zprovoznit program Habitica co nejdříve, novinky ohledně údržby budeme přidávat na náš Twitter účet. Po údržbě obdržíte Veteránského mazlíčka!", + "maintenanceInfoWhy": "Proč se to děje?", + "maintenanceInfoWhyText": "Pár posledních měsíců jsme zemi Habitica v pozadí důkladně rekonstruovali. Specificky, jsme přepsali API. I když to na povrchu možná nevypadá o tolik jinak, pod povrchem je to naprosto nový svět. V budoucnosti nám toto povolí vytvořit mnohem snadněji mnoho funkcí a zlepšit celkovou hratelnost!", + "maintenanceInfoTechDetails": "Chcete více detailů ohledně technické stránky procesu? Navštivte Kovárnu, náš blog o vývoji.", + "maintenanceInfoMore": "Více informací", + "maintenanceInfoAccountChanges": "Jaké změn, ve svém účtu, si budu moci všimnout, až bude přepis dokončen? ", + "maintenanceInfoAccountChangesText": "Nejdříve nebudou žádné viditelné změny až na zlepšení výkonu pro funkce jako např. Výzvy. Pokuď si všimneš jakékoli změny, která tam nemá být, napiš nám email na <%= hrefTechAssistanceEmail %> a my se na to podíváme!", + "maintenanceInfoAddFeatures": "Jaké vylepšení se přidají do země Habitica?", + "maintenanceInfoAddFeaturesText": "Dokončení tohoto přepisu nám dovolí začít budovat vylepšený chat a Cechy, plány pro organizace a rodiny a další produktivní funkce, například Měsíčky a možnost si nahrát včerejší aktivitu! To jsou všechno funkce, které jsou jednotlivé, takže bude trvat déle je zavést,pokud bychom však nezačali s tímto přepisem, nebylo by možné je začít provádět.", + "maintenanceInfoHowLong": "Jako dlouho bude údržba trvat?", + "maintenanceInfoHowLongText": "Musíme převést úkoly a data pro 1.3 milióny uživatelů programu Habitica - nejednoduchý úkol! Očekáváme, že se údržba bude konat od (20:00 UTC) do (05:00 UTC). Buďte si jistí, že děláme co je v našich silách, abychom vše stihli co nejrychleji. Můžete sledovat aktualizace na našem Twitteru.", + "maintenanceInfoStatsAffected": "Co se stane s mými Denními úkoly, Sériemi úspěšnosti, Bonusy a Výpravami?", + "maintenanceInfoStatsAffectedText1": "Během víkendu neutrpíte ŽÁDNÉ poškození a neztratíte vaše série úspěšnosti, váš sen se však resetuje normálně! Denní úkoly, které jste odškrtli budou odškrtnuté, bonusy se resetují, atd. Jste-li na sběratelské výpravě, stále budete nalézat předměty, jste-li v bitvě s Bossem, stále budete působit poškození, ovšem Boss nebude působit poškození vám. (I příšery potřebují pauzu!)", + "maintenanceInfoStatsAffectedText2": "Po dlouhém přemýšlení se náš tým rozhodl, že toto je nejférovější cesta jak si poradit se skutečností, že mnoho uživatelů nebude schopno si odškrtnout své Denní úkoly během údržby. Omlouváme se za veškeré nepříjemnosti!", + "maintenanceInfoSeeTasks": "Co když potřebuji vidět svůj list úkolů?", + "maintenanceInfoSeeTasksText": "Budete-li potřebovat vidět váš seznam úkolů v sobotu, abyste věděli co dělat, doporučujeme vám si ho před začátkem údržby vyfotit.", + "maintenanceInfoRarePet": "Jakého vzácného mazlíčka dostanu?", + "maintenanceInfoRarePetText": "Abychom vám poděkovali za vaší trpělivost během prostoje, každý dostane vzácného Veteránského mazlíčka. Pokud jste žádného nikdy předtím nedostali, obdržíte Veteránského vlka. Máte-li už Veteránského vlka, obdržíte Veteránského tygra. Máte-li už oba tyto mazlíčky, obdržíte Veteránského mazlíčka, kterého ještě nikdo nikdy neviděl! Může trvat i několik hodin, po dokončení migrace serveru, než se váš mazlíček objeví ale nebojte se, každý jednoho dostane.", + "maintenanceInfoWho": "Kdo pracoval na tomto obrovském projektu?", + "maintenanceInfoWhoText": "Děkujeme za optání! Bylo to vedeno naším úžasným přispěvatelem jménem paglias, s hodně pomocí od hráčů, kteří si říkají Blade, TheHollidayInn, SabreCat, Victor Pudeyev, TheUnknown, a Alys.", + "maintenanceInfoTesting": "Tato nová verze byla také neúnavně testovaná skupinou úžasných dobrovolníků. Děkujeme vám - bez vás bychom to nezvládli." } diff --git a/website/common/locales/cs/npc.json b/website/common/locales/cs/npc.json index 0be2fd52e4..dfdaa1413e 100644 --- a/website/common/locales/cs/npc.json +++ b/website/common/locales/cs/npc.json @@ -5,18 +5,18 @@ "welcomeTo": "Vítej v", "welcomeBack": "Vítej zpět!", "justin": "Justin", - "justinIntroMessage1": "Hello there! You must be new here. My name is Justin, and I'll be your guide in Habitica.", + "justinIntroMessage1": "Ahoj, ty musíš být nový/á. Jmenuji se Justin, a budu tě provázet po světě Habitica.", "justinIntroMessage2": "Pro začátek budeš potřebovat vytvořit tvojí postavu.", "justinIntroMessage3": "Skvěle! Teď - na čem by jsi rád pracoval na tvé výpravě?", - "justinIntroMessageUsername": "Before we begin, let’s figure out what to call you. Below you’ll find a display name and username I’ve generated for you. After you’ve picked a display name and username, we’ll get started by creating an avatar!", - "justinIntroMessageAppearance": "So how would you like to look? Don’t worry, you can change this later.", + "justinIntroMessageUsername": "Než začneme, musíme vymyslet, jak Ti budeme říkat. Dole uvidíš veřejné a uživatelské jméno, které jsem pro tebe vygeneroval. Poté, co si vybereš své veřejné a uživatelské jméno, začneme s tvorbou avatara!", + "justinIntroMessageAppearance": "Jak bys chtěl/a vypadat? Neboj se, můžeš svůj vzhled později změnit.", "introTour": "A jsme tu! Vyplnil jsem ti pár úkolů na základě tvých zájmů, takže můžeš ihned začít. Klikni na úkol pro jeho úpravu. nebo přidej nový úkol, který by odpovídal tvé rutině!", "prev": "Předch", "next": "Další", "randomize": "Znáhodnit", "mattBoch": "Matt Boch", "mattShall": "<%= name %>, cítíš se na projížďku? Jakmile dostatečně nakrmíš mazlíčka, objeví se tady a budeš se na něm moci projet. Klikni na zvíře, které si chceš osedlat!", - "mattBochText1": "Vítej ve stáji! Jsem Matt, pán zvířat. Od levelu 3 můžeš nalézt vejce a lektvary, kterými z nich můžeš vylíhnout mazlíčky. Když si na trhu vylíhneš mazlíčka, objeví se tady! Klikni na obrázek mazlíčka, abys ho přidal ke svému avataru. Krm je jídlem, které můžeš od levelu 3 naleznout a vyrostou ti v otužilá zvířata.", + "mattBochText1": "Vítej ve stáji! Jsem Matt, pán zvířat. Pokaždé, když dokončíš úkol, můžeš nalézt vejce a lektvary, kterými z nich můžeš vylíhnout mazlíčky. Když se vylíhne mazlíček, objeví se tady! Klikni na obrázek mazlíčka, abys ho přidal ke svému avataru. Krm je jídlem, které najdeš a vyrostou ti v otužilá zvířata.", "welcomeToTavern": "Vítej v Krčmě!", "sleepDescription": "Potřebuješ pauzu? Ubytuj se v Danielově krčmě pro pauznutí některých z těžších herních mechanismů země Habitica:", "sleepBullet1": "Promeškané denní úkoly tě nezraní", @@ -43,7 +43,7 @@ "displayPotionForGold": "Chceš prodat <%= itemType %> lektvar?", "sellForGold": "Prodat za <%= gold %> zlaťáky", "howManyToSell": "Kolik by jsi chtěl prodat?", - "yourBalance": "Tvá bilance", + "yourBalance": "Tvá bilance:", "sell": "Prodat", "buyNow": "Koupit teď", "sortByNumber": "Číslo", @@ -90,7 +90,7 @@ "pathRequired": "Je požadována cesta k vláknu", "unlocked": "Předměty byly odemčeny", "alreadyUnlocked": "Celý set je již odemčen.", - "alreadyUnlockedPart": "Celý set je již částečně odemčen.", + "alreadyUnlockedPart": "Celý set je již částečně odemčen. Je levnější koupit zbývající předměty samostatně.", "invalidQuantity": "Množství k nákupu musí být kladné celé číslo.", "USD": "(USD)", "newStuff": "Nové věci od Bailey", @@ -106,11 +106,11 @@ "card": "Platební kartou", "amazonInstructions": "Klikni pro zaplacení přes Amazon platby", "paymentMethods": "Platební metody", - "paymentSuccessful": "Your payment was successful!", + "paymentSuccessful": "Tvá platba proběhla úspěšně!", "paymentYouReceived": "Obdržel jsi:", - "paymentYouSentGems": "You sent <%= name %>:", - "paymentYouSentSubscription": "You sent <%= name %> a <%= months %>-months Habitica subscription.", - "paymentSubBilling": "Your subscription will be billed $<%= amount %> every <%= months %> months.", + "paymentYouSentGems": "Poslal/a jsi <%= name %>:", + "paymentYouSentSubscription": "Poslal/a jsi <%= name %> předplatné na <%= months %>-měsíce/ů v Habitica.", + "paymentSubBilling": "Tvoje předplatné ve výši $<%= amount %> bude účtovano každé/ých <%= months %> měsíce/ů .", "success": "Úspěch!", "classGear": "Vybavení pro tvé povolání", "classGearText": "Gratuluji k vybrání povolání! Přidal jsem ti základní zbraň do tvého inventáře. Podívej se dolů a vybav se!", @@ -118,11 +118,11 @@ "autoAllocate": "Připisovat automaticky", "autoAllocateText": "Pokud je vybrané 'Automatické přidělení', tvůj avatar dostává dovednostní body automaticky na základě nastavení tvých úkolů, které můžeš najít v: ÚKOL > Upravit > Pokročilé nastavení > Přidělení dovednostních bodů. Například, pokud často chodíš do posilovny a tvůj denní úkol 'Posilovna' je nastavený na 'Sílu', bod dovednosti se ti automaticky přidělí k Síle.", "spells": "Dovednosti", - "spellsText": "Nyní můžeš odemknout nové schopnosti tvého povolání. První uvidíš na 11 úrovni. Tvá mana se obnovuje po 10 bodech každý den, plus 1 bod za splněný Úkol.", + "spellsText": "Nyní můžeš odemknout nové schopnosti tvé třídy. První uvidíš na 11. úrovni. Tvá mana se obnovuje po 10 bodech každý den, plus 1 bod za splněný úkol.", "skillsTitle": "Dovednosti", - "toDo": "úkolu", + "toDo": "úkol", "moreClass": "Pro více informací o systému povolání, přejdi na Wikia.", - "tourWelcome": "Vítej v zemi Habitica! Tohle tvůj Úkolníček. Odškrtni si úkol abys mohl pokračovat!", + "tourWelcome": "Vítej v zemi Habitica! Tohle tvůj úkolníček. Odškrtni si úkol, abys mohl pokračovat!", "tourExp": "Skvělá práce! Odškrtnutí úkolu ti přidává Zkušenost a Zlaťáky!", "tourDailies": "Tento sloupec je pro denní úkoly. Přidej sem úkol, který bys měl plnit každý den! Příklady denních úkolů: Ustlat postel, Použít dentální nit, Zkontrolovat pracovní e-mail", "tourCron": "Úžasné! Tvé Denní úkoly se budou resetovat každý den.", @@ -138,9 +138,9 @@ "tourPartyPage": "Tvá družina ti pomůže dodržovat cíle. Pozvi své přátele a odemkni Svitek výpravy!", "tourGuildsPage": "Cechy jsou chatovací skupiny vytvořeny hráči pro hráče sdílející určité společné zájmy. Procházej list cechů a pokud se ti nějaký zalíbí, přidej se k němu. Nezapomeň se také případně podivát na populární cech Habitica Help: Ask a Question, kde se jakýkoliv hráč může zeptat na otázky ohledně Habitiky!", "tourChallengesPage": "Výzvy jsou seznamy tématických úkolů vytvořené uživateli! Přidání se k výzvě ti přidá úkoly do tvých listů. Soutěž proti ostatním uživatelům a vyhraj cenné drahokamy!", - "tourMarketPage": "Když dosáhneš úrovně 3, začneš po splnění úkolů náhodně nacházet vejce a lektvary. Budou se objevovat tady - použij je k vylíhnutí mazlíčků! Můžeš si je také koupi na Trhu.", + "tourMarketPage": "Po každém splnění úkolu máš šanci náhodně najít vejce, lektvar, nebo kus jídla pro tvé mazlíčky. Můžeš si je také koupit na trhu.", "tourHallPage": "Vítej v Síni hrdinů, kde jsou oslavování open-source přispěvatelé programu Habitica. Vysloužili si Drahokamy, exkluzivní vybavení a prestižní tituly ať už za kódování, obrázky, hudbu, psaní, nebo za pomoc. Také můžeš programu Habitica přispět!", - "tourPetsPage": "Tohle je stáj. Po dosažení levelu 3 začneš při plnění úkolů získávat vejce a líhnoucí lektvary. Když si na trhu vylíhneš mazlíčka, objeví se tady! Klikni na obrázek mazlíčka, abys ho přidal ke svému avataru. Krm je jídlem, které budeš nacházet a oni ti vyrostou v silná zvířata.", + "tourPetsPage": "Vítej ve stáji. Při splnění úkolu máš šanci získat vejce a líhnoucí lektvar k vylíhnutí mazlíčků. Když se vylíhne mazlíček, objeví se tady! Klikni na obrázek mazlíčka, abys ho přidal ke svému avataru. Krm je jídlem, které budeš nacházet a oni ti vyrostou v silná zvířata.", "tourMountsPage": "Jak nakrmíš dostatečně mazlíčka jídlem, aby se změnil na jezdecké zvíře, objeví se tady. Klikni na jezdecké zvíře k osedlání!", "tourEquipmentPage": "Tady se ti ukládá vybavení! Tvá Bojová zbroj ovlivňuje tvé statistiky. Pokud chceš, aby se ti zobrazovalo jiné vybavení na tvém avataru aniž by se ti statistiky nějak ovlivnily, klikni na \"Povolit kostým.\"", "equipmentAlreadyOwned": "Tuto část vybavení již vlastníte", @@ -149,7 +149,7 @@ "tourSplendid": "Velkolepé!", "tourNifty": "Prima!", "tourAvatarProceed": "Ukaž mi moje úkoly!", - "tourToDosBrief": "Úkolníček", + "tourToDosBrief": "Úkolníček", "tourDailiesBrief": "Denní úkoly", "tourDailiesProceed": "Budu opatrný!", "tourHabitsBrief": "Dobré zvyky a zlozvyky", @@ -170,5 +170,7 @@ "limitedOffer": "Dostupné do <%= date %>", "paymentCanceledDisputes": "Na váš e-mail jsme zaslali potvrzení o zrušení. Pokud e-mail nevidíte, kontaktujte nás, abychom předešli budoucím sporům o fakturaci.", "paymentAutoRenew": "Toto předplatné se automaticky obnoví, dokud nebude zrušeno. Pokud potřebujete předplatné zrušit, můžete tak učinit z nastavení.", - "cannotUnpinItem": "Tuto položku nelze odepnout." + "cannotUnpinItem": "Tuto položku nelze odepnout.", + "paymentSubBillingWithMethod": "Tvé předplatné $<%= amount %> bude účtováno každé/CZ <%= months %> měsíce/ů skrze <%= paymentMethod %>.", + "invalidUnlockSet": "Tento set předmětů je prošlý a nemůže být odemčen." } diff --git a/website/common/locales/cs/settings.json b/website/common/locales/cs/settings.json index dd72e4c1a4..2abe03a875 100644 --- a/website/common/locales/cs/settings.json +++ b/website/common/locales/cs/settings.json @@ -213,5 +213,6 @@ "mentioning": "Zmínka", "buyGemsGoldCapBase": "Drahokamy zastropovány na <%= amount %>", "chatExtensionDesc": "Rozšíření Chat pro Habitica přidává intuitivní chatovací okno do habitica.com. To umožňuje uživatelům povídat si v Taverně, s jejich družinou a každém cechu, ve které jsou členy.", - "chatExtension": "Rozšíření Chrome Chat a Rozšíření Firefox Chat" + "chatExtension": "Rozšíření Chrome Chat a Rozšíření Firefox Chat", + "displaynameIssueNewline": "Zobrazovaná jména nesmí obsahovat zpětné lomítko následované písmenem N." } diff --git a/website/common/locales/de/communityguidelines.json b/website/common/locales/de/communityguidelines.json index d321297df0..63a9353cad 100644 --- a/website/common/locales/de/communityguidelines.json +++ b/website/common/locales/de/communityguidelines.json @@ -24,7 +24,7 @@ "commGuideList02L": "Wir raten Dir dringend davon ab, persönliche Informationen - besonders solche, mit denen Du identifiziert werden könntest - in öffentlichen Chats zu teilen. Zu den identifizierenden Informationen gehören unter anderem: Deine Adresse, Deine E-Mail-Adresse und Dein API-Token/Passwort. Dies dient nur Deiner Sicherheit! Mitarbeiter oder Moderatoren werden solche Beiträge nach eigenem Ermessen entfernen. Wenn Du nach persönlichen Informationen in einer privaten Gilde, Party oder per PN gefragt wirst, empfehlen wir dringend, dass Du höflich ablehnst und Mitarbeiter und Moderatoren informierst, indem Du entweder 1) den Beitrag über das Fähnchen meldest, wenn er in einer Party oder privaten Gilde ist, oder 2) das Moderator-Kontaktformular ausfüllst und einen Screenshot anhängst.", "commGuidePara019": "An privaten Orten haben Benutzer die Freiheit, alle möglichen Themen zu besprechen, solange diese nicht den AGB widersprechen. Dies umfasst das Posten von diskriminierenden, gewalttätigen oder einschüchternden Inhalten. Beachte, dass Herausforderungsnamen im öffentlichen Profil des Gewinners angezeigt werden, daher müssen ALLE Herausforderungsnamen den Community-Richtlinien für öffentliche Orte entsprechen, auch wenn sie an privaten Orten genutzt werden.", "commGuidePara020": "Für private Nachrichten (PNs) gibt es einige zusätzliche Richtlinien. Falls Dich jemand geblockt hat, kontaktiere ihn nicht über andere Wege, um ihn oder sie zu bitten Dich nicht mehr zu blocken. Außerdem solltest Du keine PNs schicken, wenn Du Hilfe mit der Seite, also \"Support\" brauchst (allgemein zugängliche Antworten auf diese Fragen in der Taverne oder im Forum kommen der Gemeinschaft zugute). Schicke auch bitte keine PNs, in denen Du um Edelsteine oder ein Abonnement bettelst, da dies als Spam gewertet werden kann.", - "commGuidePara020A": "Siehst Du einen Beitrag, der Dich beunruhigt oder von dem Du glaubst, er verletze die oben zusammengefassten Community-Richtlinien für öffentliche Orte, kannst Du ihn bei Moderatoren und Mitarbeitern melden, indem Du auf das Fähnchen klickst. Ein Mitarbeiter oder Moderator wird sich dieser Meldung sobald wie möglich annehmen. Bitte beachte, dass das vorsätzliche Melden harmloser Beiträge eine Verletzung dieser Richtlinien darstellt (siehe unten unter “Regelverletzung”). PNs können derzeit nicht über das Fähnchen gemeldet werden. Um eine PN zu melden, benutze bitte das Moderatoren-Kontaktformular auf der “Kontakt”-Seite oder über “Kontaktiere das Moderatoren-Team” im Hilfe-Menü. Du kannst dies tun, wenn es mehrere problematische Beiträge derselben Person in verschiedenen Gilden gibt, oder wenn die Situation einer Erklärung bedarf. Du kannst uns in Deiner Muttersprache kontaktieren, wenn das für Dich einfacher ist: Wir müssen vielleicht Google Translate verwenden, aber wir möchten, dass Du Dich wohl fühlst, wenn Du uns ein Problem mitteilst.", + "commGuidePara020A": "Siehst Du einen Beitrag (oder eine persönliche Nachricht), der Dich beunruhigt oder von dem Du glaubst, er verletze die oben zusammengefassten Community-Richtlinien für öffentliche Orte, kannst Du ihn bei Moderatoren und Mitarbeitern melden, indem Du auf das Fähnchen klickst. Ein Mitarbeiter oder Moderator wird sich dieser Meldung sobald wie möglich annehmen. Bitte beachte, dass das vorsätzliche Melden harmloser Beiträge eine Verletzung dieser Richtlinien darstellt (siehe unten unter “Regelverletzung”). Du kannst die Moderatoren auch kontaktieren über das Moderatoren-Kontaktformular auf der “Kontakt”-Seite oder über “Kontaktiere das Moderatoren-Team” im Hilfe-Menü. Du kannst dies tun, wenn es mehrere problematische Beiträge derselben Person in verschiedenen Gilden gibt, oder wenn die Situation einer Erklärung bedarf. Du kannst uns in Deiner Muttersprache kontaktieren, wenn das für Dich einfacher ist: Wir müssen vielleicht Google Translate verwenden, aber wir möchten, dass Du Dich wohl fühlst, wenn Du uns ein Problem mitteilst.", "commGuidePara021": "Manche öffentliche Orte in Habitica haben außerdem noch weitere Regeln.", "commGuideHeadingTavern": "Die Taverne", "commGuidePara022": "Die Taverne ist der Haupttreffpunkt der Habiticaner. Daniel der Gastwirt hält das Haus blitzblank und Lemoness zaubert Dir gerne eine Limonade herbei, während Du Dich setzt und mit den anderen unterhältst. Und denk dran…", diff --git a/website/common/locales/de/questscontent.json b/website/common/locales/de/questscontent.json index fa5cf79218..9f2b8ca1c5 100644 --- a/website/common/locales/de/questscontent.json +++ b/website/common/locales/de/questscontent.json @@ -48,7 +48,7 @@ "questHarpyUnlockText": "Schaltet den Kauf von Papageieneiern auf dem Marktplatz frei", "questRoosterText": "Hahnenkampf", "questRoosterNotes": "Jahrelang nutzte der Farmer @extrajordanary Hähne als Wecker. Doch nun ist ein riesiger Hahn aufgetaucht, der lauter kräht als je einer davor - und alle Einwohner Habiticas weckt! Die unausgeschlafenen Habiticaner mühen sich durch ihre Tagesaufgaben. @Pandoro beschließt, dass nun die Zeit gekommen sei, dem ein Ende zu bereiten. \"Bitte, gibt es jemanden, der diesem Hahn beibringen kann, leise zu krähen?\" Du meldest Dich freiwillig und näherst Dich dem Hahn eines frühen Morgens - aber er dreht sich um, schlägt mit seinen gigantischen Flügeln, zeigt seine scharfen Krallen und kräht einen Schlachtruf.", - "questRoosterCompletion": "Mit Raffinesse und Stärke ist es Dir gelungen, die wilde Bestie zu zähmen. Die Ohren des Hahnes, die bisher mit Federn und halbvergessenen Aufgaben verstopft waren, sind nun offen wie ein Scheunentor. Er kräht Dich leise an und kuschelt seinen Schnabel an Deine Schulter. Am nächsten Tag willst Du wieder aufbrechen, aber @EmeraldOx rennt auf Dich zu, in der Hand einen bedeckten Korb. \"Warte! Als ich diesen Morgen ins Bauernhaus kam, hatte der Hahn dies hier an die Tür geschoben, hinter der Du geschlafen hast. Ich glaube, er will, dass Du sie bekommst.\"\nDu öffnest den Korb und siehst drei zierliche Eier.", + "questRoosterCompletion": "Mit Raffinesse und Stärke ist es Dir gelungen, die wilde Bestie zu zähmen. Die Ohren des Hahnes, die bisher mit Federn und halbvergessenen Aufgaben verstopft waren, sind nun offen wie ein Scheunentor. Er kräht Dich leise an und kuschelt seinen Schnabel an Deine Schulter. Am nächsten Tag willst Du wieder aufbrechen, aber @EmeraldOx rennt auf Dich zu, in der Hand einen bedeckten Korb. \"Warte! Als ich diesen Morgen ins Bauernhaus kam, hatte der Hahn dies hier an die Tür geschoben, hinter der Du geschlafen hast. Ich glaube, er will, dass Du sie bekommst.\" Du öffnest den Korb und siehst drei zierliche Eier.", "questRoosterBoss": "Hahn", "questRoosterDropRoosterEgg": "Hahn (Ei)", "questRoosterUnlockText": "Schaltet den Kauf von Hahneneiern auf dem Marktplatz frei", diff --git a/website/common/locales/de/settings.json b/website/common/locales/de/settings.json index 03e8c61b26..b5745bb197 100644 --- a/website/common/locales/de/settings.json +++ b/website/common/locales/de/settings.json @@ -213,5 +213,6 @@ "mentioning": "Erwähnung", "chatExtensionDesc": "Die Chat Erweiterung für Habitica fügt eine intuitive Chatbox für habitica.com hinzu. Sie erlaubt Benutzern, in der Taverne, ihrer Party und all ihren Gilden zu chatten.", "chatExtension": "Chrome Chat Erweiterung und Firefox Chat Erweiterung", - "buyGemsGoldCapBase": "Edelsteinobergrenze bei <%= amount %>" + "buyGemsGoldCapBase": "Edelsteinobergrenze bei <%= amount %>", + "displaynameIssueNewline": "Anzeigenamen dürfen keinen Backslash gefolgt von einem Buchstaben N enthalten." } diff --git a/website/common/locales/en_GB/communityguidelines.json b/website/common/locales/en_GB/communityguidelines.json index 19c8178d3b..15d7be8254 100644 --- a/website/common/locales/en_GB/communityguidelines.json +++ b/website/common/locales/en_GB/communityguidelines.json @@ -24,7 +24,7 @@ "commGuideList02L": "We highly discourage the exchange of personal information -- particularly information that can be used to identify you -- in public chat spaces. Identifying information can include but is not limited to: your address, your email address, and your API token/password. This is for your safety! Staff or moderators may remove such posts at their discretion. If you are asked for personal information in a private Guild, Party, or PM, we highly recommend that you politely refuse and alert the staff and moderators by either 1) flagging the message if it is in a Party or private Guild, or 2) filling out the Moderator Contact Form and including screenshots.", "commGuidePara019": "In private spaces, users have more freedom to discuss whatever topics they would like, but they still may not violate the Terms and Conditions, including posting slurs or any discriminatory, violent, or threatening content. Note that, because Challenge names appear in the winner's public profile, ALL Challenge names must obey the public space guidelines, even if they appear in a private space.", "commGuidePara020": "Private Messages (PMs) have some additional guidelines. If someone has blocked you, do not contact them elsewhere to ask them to unblock you. Additionally, you should not send PMs to someone asking for support (since public answers to support questions are helpful to the community). Finally, do not send anyone PMs begging for a gift of gems or a subscription, as this can be considered spamming.", - "commGuidePara020A": "If you see a post that you believe is in violation of the public space guidelines outlined above, or if you see a post that concerns you or makes you uncomfortable, you can bring it to the attention of Moderators and Staff by clicking the flag icon to report it. A Staff member or Moderator will respond to the situation as soon as possible. Please note that intentionally reporting innocent posts is an infraction of these Guidelines (see below in “Infractions”). PMs cannot be flagged at this time, so if you need to report a PM, please contact the Mods via the form on the “Contact Us” page, which you can also access via the help menu by clicking “Contact the Moderation Team.” You may want to do this if there are multiple problematic posts by the same person in different Guilds, or if the situation requires some explanation. You may contact us in your native language if that is easier for you: we may have to use Google Translate, but we want you to feel comfortable about contacting us if you have a problem.", + "commGuidePara020A": "If you see a post or private message that you believe is in violation of the public space guidelines outlined above, or if you see a post or private message that concerns you or makes you uncomfortable, you can bring it to the attention of Moderators and Staff by clicking the flag icon to report it. A Staff member or Moderator will respond to the situation as soon as possible. Please note that intentionally reporting innocent posts is an infraction of these Guidelines (see below in “Infractions”). You can also contact the Mods via the form on the “Contact Us” page, which you can also access via the help menu by clicking “Contact the Moderation Team.” You may want to do this if there are multiple problematic posts by the same person in different Guilds, or if the situation requires some explanation. You may contact us in your native language if that is easier for you: we may have to use Google Translate, but we want you to feel comfortable about contacting us if you have a problem.", "commGuidePara021": "Furthermore, some public spaces in Habitica have additional guidelines.", "commGuideHeadingTavern": "The Tavern", "commGuidePara022": "The Tavern is the main spot for Habiticans to mingle. Daniel the Innkeeper keeps the place spic-and-span, and Lemoness will happily conjure up some lemonade while you sit and chat. Just keep in mind…", diff --git a/website/common/locales/en_GB/settings.json b/website/common/locales/en_GB/settings.json index dca3c4201d..d0fde554df 100644 --- a/website/common/locales/en_GB/settings.json +++ b/website/common/locales/en_GB/settings.json @@ -213,5 +213,6 @@ "newPMNotificationTitle": "New Message from <%= name %>", "chatExtensionDesc": "The Chat Extension for Habitica adds an intuitive chat box to all of habitica.com. It allows users to chat in the Tavern, their party, and any guilds they are in.", "chatExtension": "Chrome Chat Extension and Firefox Chat Extension", - "buyGemsGoldCapBase": "Gem cap at <%= amount %>" + "buyGemsGoldCapBase": "Gem cap at <%= amount %>", + "displaynameIssueNewline": "Display Names may not contain backslashes followed by the letter N." } diff --git a/website/common/locales/hi_IN/achievements.json b/website/common/locales/hi_IN/achievements.json index f367402d94..902d1a1947 100755 --- a/website/common/locales/hi_IN/achievements.json +++ b/website/common/locales/hi_IN/achievements.json @@ -80,6 +80,6 @@ "achievementFedPet": "एक पालतू पशु को खिलाएं", "achievementHatchedPetModalText": "अपनी सूची के लिए सिर और एक हैचिंग पोशन और एक अंडे के संयोजन का प्रयास करें", "achievementHatchedPetText": "अपने पहले पालतू हैच।", - "onboardingCompleteDesc": "आपने सूची पूरी करने के लिए 5 उपलब्धियां और 100 Gold अर्जित किया।", + "onboardingCompleteDesc": "आपने सूची पूरी करने के लिए 5 उपलब्धियां और 100 Gold अर्जित किया।", "onboardingComplete": "आपने अपने ऑनबोर्डिंग कार्यों को पूरा किया!" } diff --git a/website/common/locales/hi_IN/backgrounds.json b/website/common/locales/hi_IN/backgrounds.json index 34b804da07..8d310c743d 100755 --- a/website/common/locales/hi_IN/backgrounds.json +++ b/website/common/locales/hi_IN/backgrounds.json @@ -411,5 +411,9 @@ "backgroundScribesWorkshopNotes": "Write your next great scroll in a Scribe's Workshop.", "backgroundOldFashionedBakeryText": "पुराने जमाने की बेकरी", "backgroundMedievalKitchenText": "मध्ययुगीन रसोई", - "backgrounds022019": "SET 57: जारी फरवरी 2019" + "backgrounds022019": "SET 57: जारी फरवरी 2019", + "backgroundMedievalKitchenNotes": "मध्यकालीन रसोई में तूफ़ान पकाइये.", + "backgroundValentinesDayFeastingHallText": "वेलेंटाइन दिवस दावत हॉल", + "backgroundOldFashionedBakeryNotes": "पुराने जमाने की बेकरी के बाहर स्वादिष्ट गंध का आनंद लें।", + "backgroundValentinesDayFeastingHallNotes": "वैलेंटाइन्स दिवस पर दावत हॉल में प्रेम का माहौल तो देखिये." } diff --git a/website/common/locales/it/achievements.json b/website/common/locales/it/achievements.json index 63f4045f75..29f2552b6f 100644 --- a/website/common/locales/it/achievements.json +++ b/website/common/locales/it/achievements.json @@ -8,14 +8,14 @@ "achievementLostMasterclasserText": "Completate tutte le sedici missioni della Serie di Perfezionamento e risolto il mistero del Perfezionista Perduto!", "achievementLostMasterclasserModalText": "Hai completato tutte e sedici le missioni nella serie di Missioni Masterclasser e risolto il mistero del Masterclasser Perduto!", "achievementMindOverMatter": "Il Potere della Mente", - "achievementMindOverMatterText": "Ha completato le missioni per animali da compagnia Roccia, Melma e Filo.", - "achievementMindOverMatterModalText": "Hai completato le missioni su animali domestici Roccia, Melma e Filo!", + "achievementMindOverMatterText": "Ha completato le missioni per animali Roccia, Melma e Filo.", + "achievementMindOverMatterModalText": "Hai completato le missioni su animali Roccia, Melma e Filo!", "achievementJustAddWater": "Basta Aggiungere Acqua", "achievementJustAddWaterText": "Ha completato le missioni di Polpo, Cavalluccio Marino, Seppia, Balena, Tartaruga, Nudibranco, Serpente di Mare e Delfino.", "achievementJustAddWaterModalText": "Hai completato le missioni di Polpo, Cavalluccio Marino, Seppia, Balena, Tartaruga, Nudibranco, Serpente di Mare e Delfino!", "achievementBackToBasics": "Ritorno alle Origini", - "achievementBackToBasicsText": "Ha raccolto tutti gli animali domestici basilari.", - "achievementBackToBasicsModalText": "Hai raccolto tutti gli animali domestici di base!", + "achievementBackToBasicsText": "Ha raccolto tutti gli animali base.", + "achievementBackToBasicsModalText": "Hai raccolto tutti gli animali base!", "foundNewItems": "Hai trovato nuovi oggetti!", "letsGetStarted": "Cominciamo!", "achievementAridAuthority": "Autorità arida", diff --git a/website/common/locales/it/communityguidelines.json b/website/common/locales/it/communityguidelines.json index f4331cc9c4..835a83f48c 100644 --- a/website/common/locales/it/communityguidelines.json +++ b/website/common/locales/it/communityguidelines.json @@ -24,7 +24,7 @@ "commGuideList02L": "Scoraggiamo fortemente lo scambio di informazioni personali nelle chat degli spazi pubblici, in particolare di quelle che possono essere usate per identificarti.. Le informazioni di questo tipo possono essere, ma non sono limitate a: il tuo indirizzo, il tuo indirizzo email e la tua Chiave API/password. È per la tua sicurezza! Lo staff e i moderatori potranno rimuovere post di questo tipo quando lo riterranno necessario. Se ti vengono chieste informazioni personali in una Gilda privata, in una Squadra o tramite messaggio privato, ti consigliamo caldamente di rifiutare con gentilezza e informare lo staff e i moderatori 1) segnalando il messaggio con l'apposito bottone a forma di bandiera se si trova in una Squadra o in una Gilda, oppure 2) compilando il Modulo di Contatto Moderatori e allegando le screenshot.", "commGuidePara019": "Negli spazi privati, gli utenti hanno più libertà di discutere di quello che vogliono, ma possono comunque violare i Termini e Condizioni di utilizzo. Ciò include gli insulti e qualsiasi contenuto discriminatorio, violento o minaccioso. Nota che, siccome i nomi delle Sfide appaiono sul profilo pubblico dei vincitori, i nomi di TUTTE le sfide devono rispettare le linee guida per gli spazi pubblici, ciò anche se le sfide appaiono in uno spazio privato.", "commGuidePara020": "I Messaggi Privati (MP) hanno alcune linee guida aggiuntive. Se qualcuno ti ha bloccato, non contattarlo da qualche altra parte per chiedergli di sbloccarti. Inoltre, non dovresti mandare un MP a qualcuno che richiede assistenza (dato che le risposte pubbliche alle richieste di assistenza sono utili a tutta la community). Infine, non mandare a nessuno un MP pregandolo di regalarti gemme o un abbonamento, in quanto può essere considerato spam.", - "commGuidePara020A": "Se credi che un post che hai visto violi le linee guida per gli spazi pubblici descritte qua sopra, o se vedi un post che ti preoccupa o ti mette a disagio, puoi portarlo all'attenzione dei Moderatori e dello Staff usando l'icona a forma di bandiera per segnalarlo.. Un membro dello Staff o un Moderatore si occuperà della faccenda il più presto possibile. Ricorda che segnalare intenzionalmente post innocenti è un'infrazione di queste linee guida (vedi sotto la sezione \"Infrazioni\"). Attualmente non è possibile segnalare i messaggi privati, quindi se ne hai bisogno, contatta i moderatori dal modulo presente nella pagina \"Contattaci\", a cui puoi accedere anche dal menu della guida, cliccando \"Contatta il Team dei Moderatori.\" Puoi farlo in presenza di più post problematici della stessa persona all'interno di Gilde diverse, oppure se la situazione richiede spiegazioni. Puoi contattarci nella tua lingua madre se ti è più facile: potremmo dover ricorrere a Google Translate, ma desideriamo che tu sia a tuo agio nel contattarci in caso di problemi.", + "commGuidePara020A": "Se credi che un post che hai visto violi le linee guida per gli spazi pubblici descritte qua sopra, o se vedi un post o un messaggio privato che ti preoccupa o ti mette a disagio, puoi portarlo all'attenzione dei Moderatori e dello Staff usando l'icona a forma di bandiera per segnalarlo.. Un membro dello Staff o un Moderatore si occuperà della faccenda il più presto possibile. Ricorda che segnalare intenzionalmente post innocenti è un'infrazione di queste linee guida (vedi sotto la sezione \"Infrazioni\"). Puoi contattare i moderatori dal modulo presente nella pagina \"Contattaci\", a cui puoi accedere anche dal menu della guida, cliccando \"Contatta il Team dei Moderatori.\" Puoi farlo in presenza di più post problematici della stessa persona all'interno di Gilde diverse, oppure se la situazione richiede spiegazioni. Puoi contattarci nella tua lingua madre se ti è più facile: potremmo dover ricorrere a Google Translate, ma desideriamo che tu sia a tuo agio nel contattarci in caso di problemi.", "commGuidePara021": "Inoltre, alcuni spazi pubblici in Habitica hanno delle linee guida specifiche.", "commGuideHeadingTavern": "Taverna", "commGuidePara022": "La Taverna è il punto di incontro principale degli Habitanti. Daniel il locandiere mantiene il posto sfavillante, e Lemoness sarà felice di far comparire una limonata mentre ti siedi a discutere. Tieni solo in mente…", diff --git a/website/common/locales/it/tasks.json b/website/common/locales/it/tasks.json index 8edc178be7..d3faae9346 100644 --- a/website/common/locales/it/tasks.json +++ b/website/common/locales/it/tasks.json @@ -106,7 +106,7 @@ "startDateHelp": "Imposta la data in cui l'attività sarà \"attiva\". Non sarà necessario completarla prima di quel giorno.", "streaks": "Medaglie Serie", "streakName": "<%= count %> medaglie Serie", - "streakText": "Ha completato <%= count %> serie di 21 giorni nelle Attività Giornaliere", + "streakText": "Ha completato una serie di <%= count %> giorni su 21 nelle Attività Giornaliere", "streakSingular": "Perfezionista", "streakSingularText": "Ha completato una serie di 21 giorni su una Attività Giornaliera", "perfectName": "<%= count %> Giorni Perfetti", diff --git a/website/common/locales/ja/communityguidelines.json b/website/common/locales/ja/communityguidelines.json index c7b10f1694..f9bbb35814 100644 --- a/website/common/locales/ja/communityguidelines.json +++ b/website/common/locales/ja/communityguidelines.json @@ -24,7 +24,7 @@ "commGuideList02L": "公共のチャットでは個人情報を交換しないことを強くお勧めします――特に、本人確認に使えるような情報は。個人情報の一例としては、次のようなものが含まれます:あなたの住所、メールアドレス、API トークン/パスワード。これはあなたの安全のためにお伝えしています! スタッフやモデレーターは各自の裁量により、そのような情報を含む投稿を削除することができます。もしあなたが非公開のギルドやパーティー、またはプライベート メッセージで個人情報を求められた際は、丁重に断ったうえで、以下の両方の方法でスタッフとモデレータに知らせることを強く推奨します。1) 非公開のパーティーやギルドの場合は該当のメッセージを報告する。2)スクリーンショットを添えてモデレーターに報告フォームから送信する。", "commGuidePara019": "プライベート スペースにおいては、より自由な話題で議論することができます。ですが、中傷的、差別的、暴力的、または脅迫的な内容を投稿するなどの利用規約違反は許されません。 チャレンジの名前は、勝者の公開プロフィールに表示されますので、すべてのチャレンジの名前は、たとえそれがプライベートスペース内のものだったとしても、公開スペースガイドラインを守らなくてはならないことを付け加えておきます。", "commGuidePara020": "プライベート メッセージ(PM)には、追加のガイドラインがあります。あなたがだれかにブロックされた場合、他のどんな手段であれ連絡してブロックの取り消しを求めることは禁止です。また、だれかにサポートに関するPMを送るべきではありません(サポートに関する質問と回答は、コミュニティ全体に役立つものだからです)。最後に、贈り物やジェム、寄付を求めるPMは、スパム行為とみなされます。", - "commGuidePara020A": "もしあなたが上記の公共スペースのガイドラインに違反すると思う投稿を見つけた場合、またはあなたを心配にさせたり居心地を悪くさせたりするような投稿を見つけたら、報告アイコンを押してその投稿を報告することでモデレーターやスタッフの注意を喚起させることができます。スタッフかモデレーターの一人が、可能な限りすぐに応答するでしょう。罪のない投稿を故意に報告することはガイドラインの違反行為にあたりますので留意してください(以下の「違反行為」の項目を参照のこと)。現在プライベート メッセージは報告することができませんので、「お問い合わせ」ページ経由か、ヘルプメニューで「モデレーターに報告」をクリックしてモデレーターに連絡を取ってください。複数個所で同一人物による多重投稿が行われている場合も報告したくなるかもしれません。もし状況を説明する必要があり、その方がやりやすいのであれば、あなたの母国語で報告してくださって構いません。私たちはグーグル翻訳を使わなければいけないかもしれませんが、もし問題を抱えているのなら、気軽に私たちに連絡してほしいのです。", + "commGuidePara020A": "もしあなたが上記のガイドラインに違反すると思う公共スペースの投稿またはプライベートメッセージを見つけた場合、またはあなたを心配にさせたり不快にするような投稿を見つけたら、報告アイコンを押してその投稿またはプライベートメッセージを報告することでモデレーターやスタッフの注意を喚起させることができます。スタッフかモデレーターの一人が、可能な限りすぐに応答するでしょう。罪のない投稿を故意に報告することはガイドラインの違反行為にあたりますので留意してください(以下の「違反行為」の項目を参照)。「お問い合わせ」ページ経由か、ヘルプメニューで「モデレーターに報告」をクリックしてモデレーターに連絡を取ることもできます。複数個所で同一人物による多重投稿が行われている場合も報告したくなるかもしれません。もし状況を説明する必要があり、その方がやりやすいのであれば、あなたの母国語で報告してくださって構いません。私たちはグーグル翻訳を使わなければいけないかもしれませんが、もし問題を抱えているのなら、気軽に私たちに連絡してほしいのです。", "commGuidePara021": "さらに、Habiticaの一部のパブリックスペースは、追加のガイドラインがあります。", "commGuideHeadingTavern": "キャンプ場", "commGuidePara022": "キャンプ場は、Habitica ユーザーが交流する主な場所です。\"管理人 Daniel\" はその場所を清潔に保ち、Lemoness はあなたが座って話す間、タイミングよくレモネードを出します。ちょっと心に留めておいてください…", diff --git a/website/common/locales/ja/gear.json b/website/common/locales/ja/gear.json index 00062839df..087bc8a94b 100644 --- a/website/common/locales/ja/gear.json +++ b/website/common/locales/ja/gear.json @@ -1914,5 +1914,9 @@ "headArmoireDeerstalkerCapNotes": "この帽子は田舎への小旅行時におすすめですが、事件を解決する時にかぶることもできます!知能が<%= int %>上がります。ラッキー宝箱:探偵セット(4個中1個目のアイテム)。", "headArmoireDeerstalkerCapText": "ハンチング帽", "headArmoireAstronomersHatNotes": "天体観測や魔法学校の入学にもってこいの帽子です。体質が<%= con %>上がります。ラッキー宝箱:天文学魔道士セット(3個中2個目のアイテム)。", - "headArmoireAstronomersHatText": "天文学者の帽子" + "headArmoireAstronomersHatText": "天文学者の帽子", + "armorArmoireInvernessCapeNotes": "この丈夫な服なら、どんな天気でも手がかりを探せます。知覚と知能がそれぞれ<%= attrs %>上がります。ラッキー宝箱:探偵セット(4個中2個目のアイテム)。", + "armorArmoireInvernessCapeText": "インバネスコート", + "weaponArmoireMagnifyingGlassNotes": "おや!証拠品の一部だ!近づいてこの虫眼鏡で調べてみましょう。知覚が<%= per %>上がります。ラッキー宝箱:探偵セット(4個中3個目のアイテム)。", + "weaponArmoireMagnifyingGlassText": "虫眼鏡" } diff --git a/website/common/locales/ja/settings.json b/website/common/locales/ja/settings.json index eae97968f6..4f6daa85d9 100644 --- a/website/common/locales/ja/settings.json +++ b/website/common/locales/ja/settings.json @@ -213,5 +213,6 @@ "suggestMyUsername": "ユーザー名を提案してもらう", "chatExtensionDesc": "Habiticaのチャット拡張機能は、すべてのhabitica.comに直感的なチャットボックスを追加します。キャンプ場や参加しているパーティー・ギルドでユーザーがチャットできます。", "chatExtension": "Chromeのチャット拡張機能Firefoxのチャット拡張機能", - "buyGemsGoldCapBase": "ジェムの購入可能数は<%= amount %>です" + "buyGemsGoldCapBase": "ジェムの購入可能数は<%= amount %>です", + "displaynameIssueNewline": "表示名はバックスラッシュとNの文字を連続して使うことができません。" } diff --git a/website/common/locales/pl/content.json b/website/common/locales/pl/content.json index 0305358f85..71d2c0d9d4 100644 --- a/website/common/locales/pl/content.json +++ b/website/common/locales/pl/content.json @@ -201,7 +201,7 @@ "hatchingPotionThunderstorm": "Burzowy", "hatchingPotionGhost": "Duch", "hatchingPotionRoyalPurple": "Purpura królewska", - "hatchingPotionHolly": "Ostrokrzewowy", + "hatchingPotionHolly": "Świąteczny", "hatchingPotionCupid": "Amor", "hatchingPotionShimmer": "Migoczący", "hatchingPotionFairy": "Bajeczny", diff --git a/website/common/locales/pl/gear.json b/website/common/locales/pl/gear.json index feba6c3510..6ec64844ab 100644 --- a/website/common/locales/pl/gear.json +++ b/website/common/locales/pl/gear.json @@ -1764,5 +1764,7 @@ "armorMystery201906Text": "Ogon Uprzejmego Karpia", "eyewearMystery201907Text": "Słodkie Okulary Przeciwsłoneczne", "eyewearMystery201907Notes": "Wyglądają niesamowicie, a dodatkowo chronią oczy przed promieniami UV! Brak dodatkowych korzyści. Przedmiot subskrybenta, lipiec 2019.", - "headSpecialWinter2020MageNotes": "Oh! Jak dzwonki / Słodkie złote dzwonki / Wszystkie wydają się mówić, / “Rzuć ‘Eksplozję Płomieni’” (w oparciu o słowa piosenki \"Carol of the Bells\" - przyp. tłum.) Zwiększa Percepcję o <%= per %>. Edycja Limitowana 2019-2020 Zimowe Wyposażenie." + "headSpecialWinter2020MageNotes": "Oh! Jak dzwonki / Słodkie złote dzwonki / Wszystkie wydają się mówić, / “Rzuć ‘Eksplozję Płomieni’” (w oparciu o słowa piosenki \"Carol of the Bells\" - przyp. tłum.) Zwiększa Percepcję o <%= per %>. Edycja Limitowana 2019-2020 Zimowe Wyposażenie.", + "armorMystery202006Text": "Wielobarwny Ogon Syreny", + "armorMystery202006Notes": "Nawet pośród najjaśniejszych koralowców i ukwiałów, ogon ten dumnie wyróżnia się z tłumu! Nie przynosi korzyści. Przedmiot Subskrybenta, czerwiec 2020 r." } diff --git a/website/common/locales/pl/subscriber.json b/website/common/locales/pl/subscriber.json index f9c6a6b622..9bfbfb0ccf 100644 --- a/website/common/locales/pl/subscriber.json +++ b/website/common/locales/pl/subscriber.json @@ -251,5 +251,5 @@ "subscribersReceiveBenefits": "Abonenci otrzymują przydatne korzyści!", "giftASubscription": "Podaruj Abonament", "mysterySet202007": "Zestaw Wybitnej Orki", - "mysterySet202006": "Zestaw Wielobarwnego Merfolka" + "mysterySet202006": "Zestaw Wielobarwnej Syreny" } diff --git a/website/common/locales/pt_BR/communityguidelines.json b/website/common/locales/pt_BR/communityguidelines.json index e6bdb7552b..534aed7695 100644 --- a/website/common/locales/pt_BR/communityguidelines.json +++ b/website/common/locales/pt_BR/communityguidelines.json @@ -24,7 +24,7 @@ "commGuideList02L": "Nós desencorajamos a troca de informações pessoais - particularmente informações que podem ser usadas para identificá-lo - em espaços públicos de bate-papo.Informações de identificação pode incluir, mas não se limita a: seu endereço pessoal ou profissional, seu e-mail, seu API token ou sua senha. Esta é uma regra para sua segurança! A Equipe ou Moderadores do Habitica podem remover postagens que contenham informações pessoais quando acharem necessário (mas lembre-se que alguém pode ter copiado antes, logo o mais seguro é não compartilhar). Se lhe pedirem informações pessoais em uma Guilda ou por DM, recomendamos que recuse educadamente o pedido e alerte um moderador por meio da 1) opção de reportar a mensagem ou 2) preenchendo o Formulário de contato aos Moderadores incluindo capturas de tela.", "commGuidePara019": "Em espaços privados os usuários tem maior liberdade para discutir qualquer tema que os interessem, mas ainda não devem violar os Termos e Condições, incluindo qualquer postagem ofensiva, discriminatória, violenta ou ameaçadora. Aviso: Desafios concluídos aparecem no perfil público do vencedor, de maneira que QUALQUER nome de desafio deve respeitar as Diretrizes de Espaço Público, mesmo que sejam postadas em um local privado.", "commGuidePara020": "Mensagens Diretas (DM) possuem algumas diretrizes adicionais. Caso alguém tenha bloqueado você, não entre em contato em outros lugares para pedir que essa pessoa te desbloqueie. Adicionalmente, você não deve mandar DM para alguém pedindo por ajuda (uma vez que respostas públicas à perguntas feitas são benéficas para a comunidade). Por fim, não envie DM a ninguém implorando por presentes, gemas ou por uma assinatura, pois isso pode ser considerado spam.", - "commGuidePara020A": "Se você notar uma postagem a qual acredita ser uma violação das Diretrizes de Espaço Público descritas acima, ou se você vir uma postagem que o preocupa ou o incomode, você pode alertar os Moderadores e a Equipe ao clicar em Reportar. Um membro da Equipe ou Moderador irá responde-lo o mais breve possível. Por favor, note que marcar postagens inocentes intencionalmente é uma infração dessas Diretrizes (veja abaixo em \"Infrações\"). Atualmente, não há o ícone de Reportar uma DM, então caso for necessário reportar uma DM, por favor capture a tela e contate um dos Moderadores por meio do formulário disponível na opção \"Fale Conosco\" no final das páginas do Habitica, ou indo no menu de Ajuda e clicando na opção \"Contate os Moderadores.\" Você pode precisar fazer isso se houver várias mensagens problemáticas da mesma pessoa em diferentes Guildas, ou se a situação necessitar de ser explicada. É possível nos contactar na sua língua nativa se assim for mais fácil: pode ser que usemos o Google Tradutor para lhe entender, mas o mais importante é que esteja confortável para nos avisar caso tenha algum problema.", + "commGuidePara020A": "Se você notar uma postagem ou mensagem privada a qual acredita ser uma violação das Diretrizes de Espaço Público descritas acima, ou se você vir uma postagem ou mensagem privada que o preocupa ou o incomode, você pode alertar os Moderadores e a Equipe ao clicar em Reportar. Um membro da Equipe ou Moderador irá responde-lo o mais breve possível. Por favor, note que reportar postagens inocentes intencionalmente é uma infração dessas Diretrizes (veja abaixo em \"Infrações\"). Você também pode contactar um dos Moderadores por meio do formulário disponível na opção \"Fale Conosco\" no final das páginas do Habitica, ou indo no menu de Ajuda e clicando na opção \"Contate os Moderadores.\" Você pode precisar fazer isso se houver várias mensagens problemáticas da mesma pessoa em diferentes Guildas, ou se a situação precisar de alguma explicação. É possível nos contactar na sua língua nativa se assim for mais fácil: pode ser que usemos o Google Tradutor para lhe entender, mas o mais importante é que esteja confortável para nos avisar caso tenha algum problema.", "commGuidePara021": "Além disso, alguns espaços públicos no Habitica tem regras adicionais.", "commGuideHeadingTavern": "A Taverna", "commGuidePara022": "A Taverna é o principal lugar para os Habiticanos socializarem. Daniel, o dono da pousada, mantém o lugar nos trinques, e Lemoness ficará feliz em te fazer uma limonada enquanto você senta e conversa. Mas tenha em mente que…", diff --git a/website/common/locales/pt_BR/settings.json b/website/common/locales/pt_BR/settings.json index ddd1813b43..9a9f12722d 100644 --- a/website/common/locales/pt_BR/settings.json +++ b/website/common/locales/pt_BR/settings.json @@ -213,5 +213,6 @@ "mentioning": "Menção", "chatExtensionDesc": "As extensões de bate-papo para o Habitica adiciona uma caixa intuitiva de conversação para todos os espaços sociais do habitica.com. Isso permite que os usuários conversem na Taverna, em seu grupo e nas guildas as quais fazem parte.", "chatExtension": "Extensão de Bate-papo para o Chrome e Extensão de bate-papo para o Firefox", - "buyGemsGoldCapBase": "Limite de gemas em <%= amount %>" + "buyGemsGoldCapBase": "Limite de gemas em <%= amount %>", + "displaynameIssueNewline": "Os nomes de exibição não podem conter barras invertidas seguidas pela letra N." } diff --git a/website/common/locales/vi/backgrounds.json b/website/common/locales/vi/backgrounds.json index 3763ac04cf..9a15cd75b1 100755 --- a/website/common/locales/vi/backgrounds.json +++ b/website/common/locales/vi/backgrounds.json @@ -168,7 +168,7 @@ "backgrounds052016": "SET 24: Ra mắt vào tháng Năm 2016", "backgroundBeehiveText": "Tổ ong", "backgroundBeehiveNotes": "Kêu vo vo và nhảy múa trong Tổ ong.", - "backgroundGazeboText": "Gazebo", + "backgroundGazeboText": "Vọng lâu", "backgroundGazeboNotes": "Chiến đấu với Gazebo.", "backgroundTreeRootsText": "Rễ cây", "backgroundTreeRootsNotes": "Khám phá Rễ cây.", @@ -220,44 +220,44 @@ "backgroundBlueText": "Xanh dương", "backgroundBlueNotes": "Nền Xanh dương căn bản.", "backgroundGreenText": "Xanh lá", - "backgroundGreenNotes": "Nền Xanh lá tuyệt vời", + "backgroundGreenNotes": "Một phông nền xanh lá tuyệt vời.", "backgroundPurpleText": "Tím", - "backgroundPurpleNotes": "Nền tím dễ chịu", + "backgroundPurpleNotes": "Một phông nền tím mộng mơ.", "backgroundRedText": "Đỏ", - "backgroundRedNotes": "Nền đỏ thú vị", + "backgroundRedNotes": "Một phông nền đo đỏ.", "backgroundYellowText": "Vàng", - "backgroundYellowNotes": "Nền vàng ngon lành", + "backgroundYellowNotes": "Một phông nền vàng chanh sả.", "backgrounds122016": "SET 31: Ra mắt vào tháng Mười Hai 2016", "backgroundShimmeringIcePrismText": "Lăng Kính Băng Lấp Lánh", - "backgroundShimmeringIcePrismNotes": "Nhảy múa trong những Lăng Kính Băng Lấp Lánh", + "backgroundShimmeringIcePrismNotes": "Nhảy múa trong những Lăng Kính Băng Lấp Lánh.", "backgroundWinterFireworksText": "Pháo hoa mùa Đông", - "backgroundWinterFireworksNotes": "Đốt Pháo hoa trong Mùa đông lạnh giá", + "backgroundWinterFireworksNotes": "Đốt Pháo hoa mùa Đông.", "backgroundWinterStorefrontText": "Cửa hàng mùa Đông", - "backgroundWinterStorefrontNotes": "Mua quà từ cửa hàng mùa Đông", + "backgroundWinterStorefrontNotes": "Mua quà từ một Cửa hàng mùa Đông.", "backgrounds012017": "SET 32: Ra mắt vào tháng Một 2017", "backgroundBlizzardText": "Bão Tuyết", - "backgroundBlizzardNotes": "Dũng cảm vượt qua trận Bão Tuyết dữ dội", + "backgroundBlizzardNotes": "Dũng cảm vượt qua một trận Bão Tuyết dữ dội.", "backgroundSparklingSnowflakeText": "Bông Tuyết Lấp Lánh", - "backgroundSparklingSnowflakeNotes": "Trượt trên những Bông Tuyết Lấp Lánh", + "backgroundSparklingSnowflakeNotes": "Trượt trên những Bông Tuyết Lấp Lánh.", "backgroundStoikalmVolcanoesText": "Núi Lửa Stoïkalm", - "backgroundStoikalmVolcanoesNotes": "Khám phá Núi Lửa Stoïkalm", + "backgroundStoikalmVolcanoesNotes": "Khám phá Núi Lửa Stoïkalm.", "backgrounds022017": "SET 33: Ra mắt vào tháng Hai 2017", "backgroundBellTowerText": "Tháp chuông", - "backgroundBellTowerNotes": "Leo đến Tháp chuông", + "backgroundBellTowerNotes": "Leo đến Tháp Chuông.", "backgroundTreasureRoomText": "Phòng kho báu", - "backgroundTreasureRoomNotes": "Đắm mình trong sự giàu sang của Phòng Kho Báu", + "backgroundTreasureRoomNotes": "Đắm mình trong sự giàu sang của một Phòng Kho Báu.", "backgroundWeddingArchText": "Cổng chào Lễ Cưới", - "backgroundWeddingArchNotes": "Tạo dáng dưới Cổng chào Lễ Cưới", + "backgroundWeddingArchNotes": "Tạo dáng dưới Cổng chào Lễ Cưới.", "backgrounds032017": "SET 34: Ra mắt vào tháng Ba 2017", "backgroundMagicBeanstalkText": "Cây Đậu Thần", - "backgroundMagicBeanstalkNotes": "Cùng Cây Đậu Thần vươn đến bầu trời", + "backgroundMagicBeanstalkNotes": "Cùng Cây Đậu Thần vươn đến bầu trời.", "backgroundMeanderingCaveText": "Hang Động Quanh Co", - "backgroundMeanderingCaveNotes": "Khám phá Hang Động Quanh Co", + "backgroundMeanderingCaveNotes": "Khám phá Hang Động Quanh Co.", "backgroundMistiflyingCircusText": "Rạp Xiếc Lễ Hội", - "backgroundMistiflyingCircusNotes": "Say sưa dạo chơi trong Rạp Xiếc Lễ Hội", + "backgroundMistiflyingCircusNotes": "Say sưa dạo chơi trong Rạp Xiếc Lễ Hội.", "backgrounds042017": "SET 35: Ra mắt vào tháng Tư 2017", "backgroundBugCoveredLogText": "Khúc Gỗ Đầy Bọ", - "backgroundBugCoveredLogNotes": "Điều tra về Khúc Gỗ Đầy Bọ", + "backgroundBugCoveredLogNotes": "Điều tra về Khúc Gỗ Đầy Bọ.", "backgroundGiantBirdhouseText": "Nhà Chim Khổng Lồ", "backgroundGiantBirdhouseNotes": "Làm chú chim đậu trong Nhà Chim Khổng Lồ", "backgroundMistShroudedMountainText": "Ngọn Núi Sương Mù", diff --git a/website/common/locales/vi/communityguidelines.json b/website/common/locales/vi/communityguidelines.json index 2dbb3ed3e2..9ff14d95fa 100755 --- a/website/common/locales/vi/communityguidelines.json +++ b/website/common/locales/vi/communityguidelines.json @@ -24,7 +24,7 @@ "commGuideList02L": "Chúng tôi đặc biệt không khuyến khích việc trao đổi thông tin cá nhân -- đặc biệt là những thông tin có thể nhận dạng bạn -- trong không gian nói chuyện riêng tư. Nhận dạng thông tin có thể bao gồm nhưng không giới hạn trong: địa chỉ nhà bạn, địa chỉ email của bạn, và mã API/mật khẩu của bạn. Đây là vì sự an toàn của bạn! Nhân viên hay những Điều hành viên có thể xóa bỏ những bài viết như thế theo quyết định của họ. Nếu bạn được hỏi thông tin cá nhân ở trong một Bang Hội kín, Tổ đội hay Tin nhắn trực tiếp, chúng tôi khuyến khích bạn nên từ chối lịch sự và thông báo với Nhân viên và những Điều hành viên bằng 1) gắn cờ tin nhắn nếu nó ở trong một Tổ đội hay Bang hội kín, hay 2) điền vào Biểu mẫu Liên lạc với Điều hành viênvà bao gồm cả ảnh chụp màn hình.", "commGuidePara019": "Trong những không gian riêng tư, người dùng có thể thảo luận tự do hơn về bất kỳ chủ đề nào mà họ thích, nhưng họ vẫn không thể vi phạm các Điều khoản, bao gồm việc đăng bất kỳ tin nhắn bạo lực, phân biệt chủng tộc hay có nội dung nguy hiểm. Lưu ý rằng, bởi vì tên của thử thách xuất hiện trong hồ sơ công khai, TẤT CẢ tên của Thử thách phải tuân thủ Điều khoản về Các nơi Công cộng, kể cả khi nó xuất hiện trong một không gian riêng tư.", "commGuidePara020": " Tin nhắn trực tiếp (PMs) có một số điều khoản thêm. Nếu ai đó chặn bạn, thì đừng liên lạc với họ ở nơi khác để xin họ bỏ chặn bạn. Đồng thời. bạn không nên gửi tin nhắn trực tiếp đến người khác dể xin hỗ trợ ( bởi vì những câu trả lời công khai cho những câu hỏi hỗ trợ đó rất có ích đối với cộng đồng ). Cuối cùng, làm ơn đừng gửi tin nhắn trực tiếp tới ai đó để xin họ tặng Đá quý hoặc theo dõi bạn, vì đây sẽ được coi là hành động spam.", - "commGuidePara020A": "Nếu bạn thấy một bài viết mà bạn tin rằng là đã vi phạm Quy định của không gian công cộng được nêu ra ở trên đây, hoặc bạn thấy một bài viết có liên quan tới bạn hoặc làm bạn không thoải mái, bạn có thể đưa nó tới tầm mắt của những Điều hành viên và Nhân viên bằng cách bấm vào nút lá cờ để báo cáo nó. Một Nhân viên hoặc Điều hành viên sẽ trả lời vấn đề nhanh nhất có thể. Vui lòng lưu ý rằng cố ý báo cáo bài viết bình thường là một sự vi phạm trong các Quy định (xem phần dưới trong \"Những hành vi vi phạm\"). Những tin nhắn riêng tư không thể gắn cờ ở thời điểm này, vì vậy nếu bạn cần báo cáo một tin nhắn riêng, vui lòng liên hệ những Điều hành viên bằng biểu mẫu ở trang \"Liên hệ với chúng tôi\", nơi bạn có thể truy cập từ menu giúp đỡ bằng cách bấm vào \"Liên hệ với Đội ngũ Quản trị viên.\" Bạn có thể muốn làm việc này nếu có nhiều bài viết có vấn đề được tạo bởi cùng một người trong những Bang hội khác nhau, hoặc nếu vấn đề cần giải thích. Bạn có thể liên lạc với chúng tôi bằng ngôn ngữ mẹ đẻ của mình nếu như điều đó dễ dàng hơn với bạn: chúng tôi có thể phải dùng Google Dịch, nhưng chúng tôi muốn bạn cảm thấy thoải mái về việc liên lạc với chúng tôi nếu bạn có một vấn đề.", + "commGuidePara020A": "Nếu bạn thấy một bài viết mà bạn tin rằng là đã vi phạm Quy định của không gian công cộng được nêu ra ở trên đây, hoặc bạn thấy một bài viết có liên quan tới bạn hoặc làm bạn không thoải mái, bạn có thể đưa nó tới tầm mắt của những Điều hành viên và Nhân viên bằng cách bấm vào nút lá cờ để báo cáo nó. Một Nhân viên hoặc Điều hành viên sẽ trả lời vấn đề nhanh nhất có thể. Vui lòng lưu ý rằng cố ý báo cáo bài viết bình thường là một sự vi phạm trong các Quy định (xem phần dưới trong \"Những hành vi vi phạm\"). Bạn cũng có thể liên lạc với Điều hành viên bằng biểu mẫu ở trang \"Liên hệ với chúng tôi\", nơi bạn có thể truy cập từ mục giúp đỡ bằng cách bấm vào \"Liên hệ với Đội ngũ Quản trị viên.\" Bạn có thể muốn làm việc này nếu có nhiều bài viết có vấn đề được tạo bởi cùng một người trong những Bang hội khác nhau, hoặc nếu vấn đề cần giải thích. Bạn có thể liên lạc với chúng tôi bằng ngôn ngữ mẹ đẻ của mình nếu như điều đó dễ dàng hơn với bạn: chúng tôi có thể phải dùng Google Dịch, nhưng chúng tôi muốn bạn cảm thấy thoải mái về việc liên lạc với chúng tôi nếu bạn có vấn đề.", "commGuidePara021": "Hơn nữa, một số khu vực công cộng ở Habitica có những điều khoản phụ.", "commGuideHeadingTavern": "Quán ăn", "commGuidePara022": "Quán Trọ là nơi chính cho những Habitican hòa nhập. Người giữ quán trọ Daniel giữ cho nơi này được sạch sẽ và thoáng mát, và Lemoness sẽ vui vẻ gợi ý cho bạn vài cốc nước chanh khi bạn ngồi và chém gió. Chỉ cần nhớ rằng…", diff --git a/website/common/locales/vi/quests.json b/website/common/locales/vi/quests.json index 1ca46ba146..005ee9652d 100755 --- a/website/common/locales/vi/quests.json +++ b/website/common/locales/vi/quests.json @@ -47,7 +47,7 @@ "itemsToCollect": "Items to Collect", "bossDmg1": "Mỗi lần hoàn thành Việc hằng ngày và Việc cần làm và mỗi Thói quen tích cực gây sát thương boss. Gây sát thương nó nhiều hơn với công việc màu đỏ hoặc Brutal Smash và Burst of Flames. Boss sẽ gây sát thương cho mỗi người tham gia nhiệm vụ cho mỗi ngày bạn đã bỏ lỡ (nhân với sức mạnh của boss) ngoài những sát thương thông thường của bạn, vì vậy giữ cho nhóm của bạn lành mạnh bằng cách hoàn thành Việc hằng ngày! Tất cả các sát thương đến và từ boss được tính theo cron (thời điểm bắt đầu ngày mới) .", "bossDmg2": "Only participants will fight the boss and share in the quest loot.", - "bossDmg1Broken": "Mỗi lần hoàn thành Việc hằng ngày và Việc cần làm và mỗi Thói quen tích cực gây sát thương boss. Gây sát thương nó nhiều hơn với công việc màu đỏ hoặc Brutal Smash và Burst of Flames. Boss sẽ gây sát thương cho mỗi người tham gia nhiệm vụ cho mỗi ngày bạn đã bỏ lỡ (nhân với sức mạnh của boss) ngoài những sát thương thông thường của bạn, vì vậy giữ cho nhóm của bạn lành mạnh bằng cách hoàn thành Việc hằng ngày! Tất cả các sát thương đến và từ boss được tính theo cron (thời điểm bắt đầu ngày mới) .", + "bossDmg1Broken": "Mỗi lần hoàn thành Việc hằng ngày và Việc cần làm và mỗi Thói quen tích cực gây sát thương cho quái. Gây sát thương nó nhiều hơn với công việc màu đỏ hoặc Cú chém tàn bạo và Ngọn lửa bùng nổ .Quái vật sẽ gây sát thương cho mỗi người tham gia nhiệm vụ cho mỗi Việc hàng ngày bạn đã bỏ lỡ (nhân với Sức mạnh của quái vật) ngoài những sát thương thông thường của bạn, vì vậy giữ cho Tổ đội của bạn khỏe mạnh bằng cách hoàn thành Việc hằng ngày... Tất cả các sát thương đến từ Quái vật được tung ra ra theo cron (thời điểm bắt đầu ngày mới)...", "bossDmg2Broken": "Only participants will fight the boss and share in the quest loot...", "tavernBossInfo": "Complete Dailies and To-Dos and score positive Habits to damage the World Boss! Incomplete Dailies fill the Rage Bar. When the Rage bar is full, the World Boss will attack an NPC. A World Boss will never damage individual players or accounts in any way. Only active accounts not resting in the Inn will have their tasks tallied.", "tavernBossInfoBroken": "Complete Dailies and To-Dos and score positive Habits to damage the World Boss... Incomplete Dailies fill the Exhaust Strike Bar... When the Exhaust Strike bar is full, the World Boss will attack an NPC... A World Boss will never damage individual players or accounts in any way... Only active accounts not resting in the Inn will have their tasks tallied...", @@ -130,5 +130,6 @@ "chatBossDamage": "<%= username %> tấn công <%= bossName %> với <%= userDamage %> sát thương. <%= bossName %> tấn công tổ đội với <%= bossDamage %> sát thương.", "chatQuestStarted": "Nhiệm vụ của bạn, <%= questName %>, đã bắt đầu.", "questInvitationNotificationInfo": "Bạn đã được mời tham gia một Nhiệm vụ", - "hatchingPotionQuests": "Nhiệm vụ Lọ thuốc Ấp trứng Ma thuật" + "hatchingPotionQuests": "Nhiệm vụ Lọ thuốc Ấp trứng Ma thuật", + "bossDamage": "Bạn đã gây sát thương cho con quái!" } diff --git a/website/common/locales/vi/settings.json b/website/common/locales/vi/settings.json index 3f9312734d..9245d8c81b 100755 --- a/website/common/locales/vi/settings.json +++ b/website/common/locales/vi/settings.json @@ -213,5 +213,6 @@ "mentioning": "Nhắc tên", "onlyPrivateSpaces": "Chỉ trong không gian riêng tư", "everywhere": "Mọi nơi", - "suggestMyUsername": "Gợi ý tên đăng nhập của tôi" + "suggestMyUsername": "Gợi ý tên đăng nhập của tôi", + "displaynameIssueNewline": "Tên hiển thị có thể không chứa dấu chéo ngược theo sau chữ N." } diff --git a/website/common/locales/zh/communityguidelines.json b/website/common/locales/zh/communityguidelines.json index 968920564f..98d8b490e5 100644 --- a/website/common/locales/zh/communityguidelines.json +++ b/website/common/locales/zh/communityguidelines.json @@ -24,7 +24,7 @@ "commGuideList02L": "我们非常不鼓励在公共场合交换个人信息——尤其是那些能够证明自己身份的信息。那些信息包括但不限于:你的地址、电子邮箱、API令牌和密码。这是为了你的安全!工作人员或管理员会根据自己的判断移除那些信息。如果有人在私人公会、队伍或者私聊里问到你的这些私人信息,我们强烈建议你礼貌地拒绝他,并告知工作人员或者管理员中的任意一个。方法1,如果是在队伍或者私人公会里,点击帖子下方的举报。方法2,填写 管理员联系表格 ,包括截图。", "commGuidePara019": "在私人空间中,用户有更多自由讨论他们喜欢的话题,但是仍然不能违反条款和条件,包括发布任何歧视、暴力或恐吓内容。注意,由于挑战名称会出现在胜利者的公共角色信息中,所有的挑战名称必须遵守公共空间指南,即使它们是在私人空间中。", "commGuidePara020": "私人信息(私信) 有一些附加要求。如果某人屏蔽了你,请不要在任何别的地方联系对方来解除屏蔽。而且你不应该用私信来寻求帮助(因为对问题的公开回答会帮助整个社区)。最后,不要给任何人发私信要求赠送宝石或订阅者来作为礼物,因为这样的行为会被认为是在发送垃圾信息。", - "commGuidePara020A": "如果您看到一条您认为是违反公共空间指南的消息,或者您看到一条困扰您或让您不舒服的消息,您可以通过点击举报标志将其报告给管理员和工作人员。工作人员或者管理员会尽快对情况作出回应。请注意,故意举报无辜的消息也是对社区准则的一种违反哦(具体见下方的“违规”)!私信暂时还不能被举报,所以如果您需要举报一条私信,请通过“联系我们”页面上的表格与我们的管理员联系,您也可以点击“联系管理团队”,可以通过帮助菜单访问该页面。当您遇到如下情况,您也可以这样举报,比如同一人在不同公会中有多个有问题的帖子,或者需要一些情况说明。您可以用您的母语与我们联系如果这对您很方便:我们可能需要使用Google翻译,但如果您遇到问题,我们希望您能够轻松的与我们交流。", + "commGuidePara020A": "如果您看到一条您认为是违反公共空间指南的消息或私人信息,或者您看到一条困扰您或让您不舒服的消息,您可以通过点击举报标志将其报告给管理员和工作人员。工作人员或者管理员会尽快对情况作出回应。请注意,故意举报无辜的消息也是对社区准则的一种违反哦(具体见下方的“违规”)!如果您需要举报一条私信,请通过“联系我们”页面上的表格与我们的管理员联系,您也可以点击“联系管理团队”,可以通过帮助菜单访问该页面。当您遇到如下情况,您也可以这样举报,比如同一人在不同公会中有多个有问题的帖子,或者需要一些情况说明。您可以用您的母语与我们联系如果这对您很方便:我们可能需要使用Google翻译,但如果您遇到问题,我们希望您能够轻松的与我们交流。", "commGuidePara021": "此外,Habitica中的一些公共区域还有另外的准则。", "commGuideHeadingTavern": "酒馆", "commGuidePara022": "酒馆是Habitica居民主要的交流地点。酒馆主人Daniel将店里打理的一尘不染,Lemoness乐意在你坐下聊天时变出几杯柠檬水。只是要记住……", @@ -120,7 +120,7 @@ "commGuideLink01": "Habitica 帮助:提出问题: 一个玩家提问的公会!", "commGuideLink02": "百科: 最全面地汇集Habitica相关信息的地方。", "commGuideLink03": "GitHub: 报告bug或者贡献代码!", - "commGuideLink04": "Trello: 在这里提出新功能的建议。", + "commGuideLink04": "反馈表: 在这里提出新功能的建议。", "commGuideLink05": "移动端 Trello: 在这里提出关于移动端的建议。", "commGuideLink06": "艺术 Trello: 在这里提交像素画作品。", "commGuideLink07": "副本 Trello: 在这里提交新的副本剧本。", diff --git a/website/common/locales/zh/gear.json b/website/common/locales/zh/gear.json index e695cd71d3..65a385b937 100644 --- a/website/common/locales/zh/gear.json +++ b/website/common/locales/zh/gear.json @@ -881,7 +881,7 @@ "headSpecialPageHelmText": "一页头盔", "headSpecialPageHelmNotes": "锁子甲:为了时尚和实用。增加<%= per %>点感知。", "headSpecialRoguishRainbowMessengerHoodText": "俏皮彩虹信使兜帽", - "headSpecialRoguishRainbowMessengerHoodNotes": "这个明亮的兜帽散发出一种鲜艳的光芒,可以保护你不受恶劣天气的影响。增加<%= con %>点体质。", + "headSpecialRoguishRainbowMessengerHoodNotes": "发光兜帽焕发出多彩的光芒,使你不受恶劣天气的影响。增加<%= con %>点体质。", "headSpecialClandestineCowlText": "神秘斗篷", "headSpecialClandestineCowlNotes": "当你从人物中抢夺金子和战利品时,小心地隐藏你的脸!增加<%= per %>点感知。", "headSpecialSnowSovereignCrownText": "冰雪大帝的皇冠", diff --git a/website/common/locales/zh/questscontent.json b/website/common/locales/zh/questscontent.json index 796ce57261..dddd565606 100644 --- a/website/common/locales/zh/questscontent.json +++ b/website/common/locales/zh/questscontent.json @@ -483,8 +483,8 @@ "questMayhemMistiflying1DropWhitePotion": "白色孵化药水", "questMayhemMistiflying1DropArmor": "俏皮彩虹信使长袍(护甲)", "questMayhemMistiflying2Text": "Misti飞城的混乱,第2部:疾风更盛", - "questMayhemMistiflying2Notes": "Misti飞城起伏翻滚着,因为令这座城漂浮着的魔法蜜蜂们被暴风疯狂地拍打。在一通不顾一切地寻找之后,你发现愚者在一间小屋里,漫不经心地和一只被五花大绑的、气愤的骷髅头打牌。

@Katy133 高声盖过狂风的呼啸大喊着问:“怎么回事?我们打败了骷髅,但是更糟糕了!”

“那可麻烦了,”愚者赞同道,“拜托你们当个好人,别跟冰川夫人提这事。她总是威胁要不喜欢我了,因为我是个‘灾难性地不负责任’的家伙,现在这状况我怕她误会。”他一边洗牌,一边回答:“也许你们可以跟着Misti蝴蝶?它们是无实体的,所以狂风吹不走它们,而且它们趋于一窝蜂涌向威胁。”他向窗外点点头,这座城的一些守护神在那边正飞向东方。“现在我要认真打牌了——我的对手可顶着张扑克脸在玩牌呐。”", - "questMayhemMistiflying2Completion": "你跟着Misti蝴蝶进来到了风暴眼,可是风太大了,你无法冲进去。

“这个有用,”一个声音在你耳中响起,你差点从坐骑上摔下来。愚者不知怎么的坐在你背后的一截马鞍上。“我听说信使兜帽会放射光环,在恶劣天气下提供保护——非常有用,能避免飞来飞去的时候弄丢信件。来试试?”", + "questMayhemMistiflying2Notes": "背负这座城市的魔法蜜蜂们被暴风疯狂地拍打,Misti飞城起伏翻滚。经过绝望的搜寻,你终于找到了愚者,他在一间小屋里漫不经心地和一只被五花大绑的、气愤的骷髅头打牌。

@Katy133 提高嗓音试图盖过狂风的呼啸:“怎么回事?我们打败了骷髅,但情况怎么却更糟糕了!”

“真是个麻烦,”愚者赞同道,“拜托你们帮个忙,别跟冰川夫人提这事。她总是威胁要不喜欢我了,称我是个‘灾难级不负责任’的家伙,现在这状况我怕她误会。”他一边洗牌,一边回答:“也许你们可以跟着Misti蝴蝶?它们是灵魂物,狂风吹不走它们,它们会趋于一窝蜂涌向威胁。”他瞥向窗外,这座城的守护神正飞向东方。“现在我要认真点了——我的对手可顶着张扑克脸呐。”", + "questMayhemMistiflying2Completion": "你跟随着Misti蝴蝶走近风暴眼,可是风太大了,无法冲进去。

“这或许能提供帮助,”一个声音在耳边响起,你差点从坐骑上摔下来。愚者不知何时就坐在了你身后的马鞍上。“我听说信使兜帽会闪耀光芒,在恶劣天气下能有效地保护主人——避免飞来飞去时弄丢信件。你要不试试?”", "questMayhemMistiflying2CollectRedMistiflies": "红色Misti蝴蝶", "questMayhemMistiflying2CollectBlueMistiflies": "蓝色Misti蝴蝶", "questMayhemMistiflying2CollectGreenMistiflies": "绿色Misti蝴蝶", diff --git a/website/common/locales/zh/settings.json b/website/common/locales/zh/settings.json index 52a9658354..70fb5a75c8 100644 --- a/website/common/locales/zh/settings.json +++ b/website/common/locales/zh/settings.json @@ -213,5 +213,6 @@ "mentioning": "回复", "chatExtensionDesc": "Habitica的聊天扩展程序为所有habitica.com添加了直观的聊天框。它允许用户在酒馆,他们的聚会以及他们所在的任何行会中聊天。", "chatExtension": "Chrome聊天扩展Firefox聊天扩展", - "buyGemsGoldCapBase": "宝石上限是<%= amount %>" + "buyGemsGoldCapBase": "宝石上限是<%= amount %>", + "displaynameIssueNewline": "角色名不能不得包含反斜杠(/),后跟字母N。" } diff --git a/website/common/locales/zh_TW/communityguidelines.json b/website/common/locales/zh_TW/communityguidelines.json index 832f218e4e..d9f19b3032 100644 --- a/website/common/locales/zh_TW/communityguidelines.json +++ b/website/common/locales/zh_TW/communityguidelines.json @@ -24,7 +24,7 @@ "commGuideList02L": "我們非常不鼓勵在公共場合交換個人資訊,尤其是那些能夠分辨出您的個人身分的訊息。這些訊息包括但不限於:您的住址、電子信箱、API token或密碼。這是為了您的安全著想!工作人員或管理員將根據他們的判斷移除那些訊息。若有人在私人公會、隊伍或私訊中要求您提供個人訊息,我們強烈建議您禮貌地回絕他,並告知工作人員或管理員。方法1,若是在隊伍或是私人公會裡,請點擊檢舉。方法2,填寫管理員聯絡表單並附帶截圖。", "commGuidePara019": "在個人空間中,使用者能夠更自由的討論任何喜歡的話題。但是,他們仍不能違反服務條款,這包括發布任何歧視性、暴力、或恐嚇性的內容。請記住,由於挑戰名稱將出現在勝利者的個人檔案中,所以所有的挑戰名稱也必須遵守公共空間守則,即使它們只出現在私人空間中。", "commGuidePara020": "私密訊息 (私訊)也有一些附加的規範。如果有使用者封鎖您,請不要在任何別的地方與他們聯繫,並要求他們解除封鎖您。此外,您不應該向別人發送私人訊息請求協助 (因為公開回答別人的疑問,對社群是有正面幫助的)。最後,不要發送任何私人訊息要求其他人送您寶石或訂閱當禮物,像這類的訊息都會被認定為垃圾訊息。", - "commGuidePara020A": "如果您看到一則您認為是違反公共空間守則的訊息,或者您看到一則困擾您或讓您不舒服的訊息,您可以透過檢舉將其報告給管理員或工作人員。工作人員或管理員將盡快做出回應。請注意,故意檢舉無辜的訊息也是一種違反社群規範的行為喔 (具體請見下方的「違規」介紹)!私信目前暫時還無法被檢舉,所以若您需要檢舉一則私信,請通過「聯絡我們」頁面上的表格與管理員聯繫,您也可以在主菜單上點擊「聯絡管理團隊」。當您遇到如下情況,您也可以這樣檢舉:同一個人在不同公會中發布許多有問題的訊息、或是當您遇到的狀況與要額外的文字解釋時。您可以用自己的母語來與我們聯繫,如果這樣對您較方便。此時我們可能會採用Google翻譯。但我們由衷希望您在遇到任何問題時都能夠輕鬆自在地與我們交流。", + "commGuidePara020A": "如果您看到一條您認為是違反公共空間指南的消息或私人信息,或者您看到一條困擾您或讓您不舒服的消息,您可以通過點擊舉報標誌將其報告給管理員和工作人員。工作人員或者管理員會盡快對情況作出回應。請注意,故意舉報無辜的消息也是對社區準則的一種違反哦(具體見下方的“違規”)!如果您需要舉報一條私信,請通過“聯繫我們”頁面上的表格與我們的管理員聯繫,您也可以點擊“聯繫管理團隊”,可以通過幫助菜單訪問該頁面。當您遇到如下情況,您也可以這樣舉報,比如同一人在不同公會中有多個有問題的帖子,或者需要一些情況說明。您可以用您的母語與我們聯繫如果這對您很方便:我們可能需要使用Google翻譯,但如果您遇到問題,我們希望您能夠輕鬆的與我們交流。", "commGuidePara021": "此外,在Habitica裡的某些公共區域還有額外的規範。", "commGuideHeadingTavern": "酒館", "commGuidePara022": "酒館是一個Habitica鄉民的主要交流地點。酒館主人Daniel將店裡打掃得一塵不染。而Lemoness將非常樂意地在您坐下聊天時變出幾杯檸檬水。只是您要記住…", diff --git a/website/common/locales/zh_TW/settings.json b/website/common/locales/zh_TW/settings.json index 4376a75698..83afca1666 100644 --- a/website/common/locales/zh_TW/settings.json +++ b/website/common/locales/zh_TW/settings.json @@ -213,5 +213,6 @@ "chatExtension": "Chrome聊天室擴充功能Firefox聊天室附加元件", "onlyPrivateSpaces": "只在私人空間", "everywhere": "每個地方", - "buyGemsGoldCapBase": "寶石上限是<%= amount %>" + "buyGemsGoldCapBase": "寶石上限是<%= amount %>", + "displaynameIssueNewline": "角色名不能不得包含反斜杠(/),後跟字母N。" } From c10b9b79933823102d88516f4a6d1f5996295876 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2020 12:27:02 +0200 Subject: [PATCH 02/12] build(deps): bump mongoose from 5.9.24 to 5.9.25 (#12402) Bumps [mongoose](https://github.com/Automattic/mongoose) from 5.9.24 to 5.9.25. - [Release notes](https://github.com/Automattic/mongoose/releases) - [Changelog](https://github.com/Automattic/mongoose/blob/master/History.md) - [Commits](https://github.com/Automattic/mongoose/compare/5.9.24...5.9.25) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f51e2a54a6..6cd39e9b74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9227,9 +9227,9 @@ } }, "mongoose": { - "version": "5.9.24", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.9.24.tgz", - "integrity": "sha512-uxTLy/ExYmOfKvvbjn1PHbjSJg0SQzff+dW6jbnywtbBcfPRC/3etnG9hPv6KJe/5TFZQGxCyiSezkqa0+iJAQ==", + "version": "5.9.25", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.9.25.tgz", + "integrity": "sha512-vz/DqJ3mrHqEIlfRbKmDZ9TzQ1a0hCtSQpjHScIxr4rEtLs0tjsXDeEWcJ/vEEc3oLfP6vRx9V+uYSprXDUvFQ==", "requires": { "bson": "^1.1.4", "kareem": "2.3.1", diff --git a/package.json b/package.json index 15816e9025..ff35d35ca2 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "method-override": "^3.0.0", "moment": "^2.27.0", "moment-recur": "^1.0.7", - "mongoose": "^5.9.23", + "mongoose": "^5.9.25", "morgan": "^1.10.0", "nconf": "^0.10.0", "node-gcm": "^1.0.3", From 234258b41e98e15a7b7e7402fc735b508bad5bb2 Mon Sep 17 00:00:00 2001 From: Bart Enkelaar Date: Sat, 25 Jul 2020 13:22:41 +0200 Subject: [PATCH 03/12] Move from deprecated moment#zone to moment#utcOffset (#12207) * Issue 10209 - Remove read usages of zone * Issue 10209 - Add coverage on daysSince and startOfDay cron utility functions * Issue 10209 - Add unit test for daysUserHasMissed method * Issue 10209 - Remove usages of deprecated `moment.js#zone` method. * Issue 10209 - Add helper function to centralise logic Also simplify timezoneOffsetToUtc function in site.vue * Issue 10209 - Also add getUtcOffset as method on user Co-authored-by: Matteo Pagliazzi --- package-lock.json | 9 + package.json | 1 + test/api/unit/libs/cron.test.js | 68 +++---- test/api/unit/libs/preening.test.js | 2 +- test/api/unit/models/user.test.js | 49 +++-- .../integration/tasks/GET-tasks_user.test.js | 18 +- test/common/fns/getUtcOffset.test.js | 25 +++ test/common/libs/cron.test.js | 184 ++++++++++++++++++ test/common/libs/taskDefaults.test.js | 3 +- test/common/shouldDo.test.js | 14 +- test/helpers/globals.helper.js | 3 +- website/client/src/app.vue | 7 +- .../client/src/components/settings/site.vue | 14 +- website/client/src/libs/auth.js | 5 +- website/client/src/store/index.js | 6 +- website/common/script/cron.js | 29 +-- website/common/script/fns/getUtcOffset.js | 12 ++ website/common/script/index.js | 2 + website/common/script/libs/taskDefaults.js | 3 +- website/common/script/ops/scoreTask.js | 8 +- .../controllers/top-level/dataexport.js | 4 +- website/server/libs/cron.js | 6 +- website/server/libs/preening.js | 26 +-- website/server/middlewares/cron.js | 4 +- website/server/models/user/methods.js | 52 ++--- 25 files changed, 413 insertions(+), 141 deletions(-) create mode 100644 test/common/fns/getUtcOffset.test.js create mode 100644 test/common/libs/cron.test.js create mode 100644 website/common/script/fns/getUtcOffset.js diff --git a/package-lock.json b/package-lock.json index 6cd39e9b74..b01b75f536 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3008,6 +3008,15 @@ "check-error": "^1.0.2" } }, + "chai-moment": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chai-moment/-/chai-moment-0.1.0.tgz", + "integrity": "sha1-SpFoDPo6dc/aGEULK6tltIyQJAE=", + "dev": true, + "requires": { + "moment": "^2.10.6" + } + }, "chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", diff --git a/package.json b/package.json index ff35d35ca2..fa504fbe82 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "axios": "^0.19.2", "chai": "^4.1.2", "chai-as-promised": "^7.1.1", + "chai-moment": "^0.1.0", "chalk": "^4.1.0", "cross-spawn": "^7.0.3", "expect.js": "^0.3.1", diff --git a/test/api/unit/libs/cron.test.js b/test/api/unit/libs/cron.test.js index 1544a8c373..ee0271d82f 100644 --- a/test/api/unit/libs/cron.test.js +++ b/test/api/unit/libs/cron.test.js @@ -42,13 +42,13 @@ describe('cron', () => { }); it('updates user.preferences.timezoneOffsetAtLastCron', () => { - const timezoneOffsetFromUserPrefs = 1; + const timezoneUtcOffsetFromUserPrefs = -1; cron({ - user, tasksByType, daysMissed, analytics, timezoneOffsetFromUserPrefs, + user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs, }); - expect(user.preferences.timezoneOffsetAtLastCron).to.equal(timezoneOffsetFromUserPrefs); + expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1); }); it('resets user.items.lastDrop.count', () => { @@ -240,7 +240,7 @@ describe('cron', () => { user1.purchased.plan.consecutive.gemCapExtra = 0; it('does not increment consecutive benefits after the first month', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') .add(2, 'days') .toDate()); // Add 1 month to simulate what happens a month after the subscription was created. @@ -256,7 +256,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits after the second month', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months') .add(2, 'days') .toDate()); // Add 1 month to simulate what happens a month after the subscription was created. @@ -272,7 +272,7 @@ describe('cron', () => { }); it('increments consecutive benefits after the third month', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months') .add(2, 'days') .toDate()); // Add 1 month to simulate what happens a month after the subscription was created. @@ -288,7 +288,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits after the fourth month', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months') .add(2, 'days') .toDate()); // Add 1 month to simulate what happens a month after the subscription was created. @@ -304,7 +304,7 @@ describe('cron', () => { }); it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months') .add(2, 'days') .toDate()); cron({ @@ -339,7 +339,7 @@ describe('cron', () => { user3.purchased.plan.consecutive.gemCapExtra = 5; it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') .add(2, 'days') .toDate()); cron({ @@ -352,7 +352,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the middle of the period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months') .add(2, 'days') .toDate()); cron({ @@ -365,7 +365,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months') .add(2, 'days') .toDate()); cron({ @@ -378,7 +378,7 @@ describe('cron', () => { }); it('increments consecutive benefits the month after the second paid period has started', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months') .add(2, 'days') .toDate()); cron({ @@ -391,7 +391,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(5, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months') .add(2, 'days') .toDate()); cron({ @@ -404,7 +404,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the final month of the second period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(6, 'months') .add(2, 'days') .toDate()); cron({ @@ -417,7 +417,7 @@ describe('cron', () => { }); it('increments consecutive benefits the month after the third paid period has started', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months') .add(2, 'days') .toDate()); cron({ @@ -430,7 +430,7 @@ describe('cron', () => { }); it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months') .add(2, 'days') .toDate()); cron({ @@ -465,7 +465,7 @@ describe('cron', () => { user6.purchased.plan.consecutive.gemCapExtra = 10; it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') .add(2, 'days') .toDate()); cron({ @@ -478,7 +478,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(6, 'months') .add(2, 'days') .toDate()); cron({ @@ -491,7 +491,7 @@ describe('cron', () => { }); it('increments consecutive benefits the month after the second paid period has started', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months') .add(2, 'days') .toDate()); cron({ @@ -504,7 +504,7 @@ describe('cron', () => { }); it('increments consecutive benefits the month after the third paid period has started', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months') .add(2, 'days') .toDate()); cron({ @@ -517,7 +517,7 @@ describe('cron', () => { }); it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(19, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(19, 'months') .add(2, 'days') .toDate()); cron({ @@ -552,7 +552,7 @@ describe('cron', () => { user12.purchased.plan.consecutive.gemCapExtra = 20; it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') .add(2, 'days') .toDate()); cron({ @@ -565,7 +565,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(12, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(12, 'months') .add(2, 'days') .toDate()); cron({ @@ -578,7 +578,7 @@ describe('cron', () => { }); it('increments consecutive benefits the month after the second paid period has started', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months') .add(2, 'days') .toDate()); cron({ @@ -591,7 +591,7 @@ describe('cron', () => { }); it('increments consecutive benefits the month after the third paid period has started', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(25, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(25, 'months') .add(2, 'days') .toDate()); cron({ @@ -604,7 +604,7 @@ describe('cron', () => { }); it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(37, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(37, 'months') .add(2, 'days') .toDate()); cron({ @@ -641,7 +641,7 @@ describe('cron', () => { user3g.purchased.plan.consecutive.gemCapExtra = 5; it('does not increment consecutive benefits in the first month of the gift subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') .add(2, 'days') .toDate()); cron({ @@ -654,7 +654,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the second month of the gift subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months') .add(2, 'days') .toDate()); cron({ @@ -667,7 +667,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the third month of the gift subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months') .add(2, 'days') .toDate()); cron({ @@ -680,7 +680,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the month after the gift subscription has ended', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months') .add(2, 'days') .toDate()); cron({ @@ -717,7 +717,7 @@ describe('cron', () => { user6x.purchased.plan.consecutive.gemCapExtra = 15; it('increments consecutive benefits in the first month since the fix for #4819 goes live', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') .add(2, 'days') .toDate()); cron({ @@ -730,7 +730,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the second month after the fix goes live', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months') .add(2, 'days') .toDate()); cron({ @@ -743,7 +743,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the third month after the fix goes live', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months') .add(2, 'days') .toDate()); cron({ @@ -756,7 +756,7 @@ describe('cron', () => { }); it('increments consecutive benefits in the seventh month after the fix goes live', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months') .add(2, 'days') .toDate()); cron({ diff --git a/test/api/unit/libs/preening.test.js b/test/api/unit/libs/preening.test.js index c2448c5df9..d458595fe2 100644 --- a/test/api/unit/libs/preening.test.js +++ b/test/api/unit/libs/preening.test.js @@ -9,7 +9,7 @@ describe('preenHistory', () => { beforeEach(() => { // Replace system clocks so we can get predictable results clock = sinon.useFakeTimers({ - now: Number(moment('2013-10-20').zone(0).startOf('day').toDate()), + now: Number(moment('2013-10-20').utcOffset(0).startOf('day').toDate()), toFake: ['Date'], }); }); diff --git a/test/api/unit/models/user.test.js b/test/api/unit/models/user.test.js index 50c5f8cc8a..f3345e5478 100644 --- a/test/api/unit/models/user.test.js +++ b/test/api/unit/models/user.test.js @@ -761,7 +761,7 @@ describe('User Model', () => { }); }); - context('days missed', () => { + describe('daysUserHasMissed', () => { // http://forbrains.co.uk/international_tools/earth_timezones let user; @@ -769,24 +769,51 @@ describe('User Model', () => { user = new User(); }); - it('should not cron early when going back a timezone', () => { - const yesterday = moment('2017-12-05T00:00:00.000-06:00'); // 11 pm on 4 Texas - const timezoneOffset = moment().zone('-06:00').zone(); - user.lastCron = yesterday; - user.preferences.timezoneOffset = timezoneOffset; + it('correctly calculates days missed since lastCron', () => { + const now = moment(); + user.lastCron = moment(now).subtract(5, 'days'); - const today = moment('2017-12-06T00:00:00.000-06:00'); // 11 pm on 4 Texas - const req = {}; - req.header = () => timezoneOffset + 60; + const { daysMissed } = user.daysUserHasMissed(now); - const { daysMissed } = user.daysUserHasMissed(today, req); + expect(daysMissed).to.eql(5); + }); + it('uses timezone from preferences to calculate days missed', () => { + const now = moment('2017-07-08 01:00:00Z'); + user.lastCron = moment('2017-07-04 13:00:00Z'); + user.preferences.timezoneOffset = 120; + + const { daysMissed } = user.daysUserHasMissed(now); + + expect(daysMissed).to.eql(3); + }); + + it('uses timezone at last cron to calculate days missed', () => { + const now = moment('2017-09-08 13:00:00Z'); + user.lastCron = moment('2017-09-06 01:00:00+02:00'); + user.preferences.timezoneOffset = 0; + user.preferences.timezoneOffsetAtLastCron = -120; + + const { daysMissed } = user.daysUserHasMissed(now); + + expect(daysMissed).to.eql(2); + }); + + it('respects new timezone that drags time into same day', () => { + user.lastCron = moment('2017-12-05T00:00:00.000-06:00'); + user.preferences.timezoneOffset = 360; + const today = moment('2017-12-06T00:00:00.000-06:00'); + const requestWithMinus7Timezone = { header: () => 420 }; + + const { daysMissed } = user.daysUserHasMissed(today, requestWithMinus7Timezone); + + expect(user.preferences.timezoneOffset).to.eql(420); expect(daysMissed).to.eql(0); }); it('should not cron early when going back a timezone with a custom day start', () => { const yesterday = moment('2017-12-05T02:00:00.000-08:00'); - const timezoneOffset = moment().zone('-08:00').zone(); + const timezoneOffset = 480; user.lastCron = yesterday; user.preferences.timezoneOffset = timezoneOffset; user.preferences.dayStart = 2; diff --git a/test/api/v3/integration/tasks/GET-tasks_user.test.js b/test/api/v3/integration/tasks/GET-tasks_user.test.js index e82a09e00b..85602ded26 100644 --- a/test/api/v3/integration/tasks/GET-tasks_user.test.js +++ b/test/api/v3/integration/tasks/GET-tasks_user.test.js @@ -153,12 +153,12 @@ describe('GET /tasks/user', () => { }); xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => { - const timezone = 420; + const timezoneOffset = 420; await user.update({ 'preferences.dayStart': 0, - 'preferences.timezoneOffset': timezone, + 'preferences.timezoneOffset': timezoneOffset, }); - const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day') + const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day') .toISOString(); await user.post('/tasks/user', [ { @@ -180,12 +180,12 @@ describe('GET /tasks/user', () => { }); xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => { - const timezone = 240; + const timezoneOffset = 240; await user.update({ 'preferences.dayStart': 0, - 'preferences.timezoneOffset': timezone, + 'preferences.timezoneOffset': timezoneOffset, }); - const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day') + const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day') .toISOString(); await user.post('/tasks/user', [ { @@ -207,12 +207,12 @@ describe('GET /tasks/user', () => { }); xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => { - const timezone = 540; + const timezoneOffset = 540; await user.update({ 'preferences.dayStart': 0, - 'preferences.timezoneOffset': timezone, + 'preferences.timezoneOffset': timezoneOffset, }); - const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day') + const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day') .toISOString(); await user.post('/tasks/user', [ { diff --git a/test/common/fns/getUtcOffset.test.js b/test/common/fns/getUtcOffset.test.js new file mode 100644 index 0000000000..a91c886407 --- /dev/null +++ b/test/common/fns/getUtcOffset.test.js @@ -0,0 +1,25 @@ +import getUtcOffset from '../../../website/common/script/fns/getUtcOffset'; + +describe('getUtcOffset', () => { + let user; + + beforeEach(() => { + user = { preferences: {} }; + }); + + it('returns 0 when user.timezoneOffset is not set', () => { + expect(getUtcOffset(user)).to.equal(0); + }); + + it('returns 0 when user.timezoneOffset is zero', () => { + user.preferences.timezoneOffset = 0; + + expect(getUtcOffset(user)).to.equal(0); + }); + + it('returns the opposite of user.timezoneOffset', () => { + user.preferences.timezoneOffset = -10; + + expect(getUtcOffset(user)).to.eql(10); + }); +}); diff --git a/test/common/libs/cron.test.js b/test/common/libs/cron.test.js new file mode 100644 index 0000000000..0894a6afa3 --- /dev/null +++ b/test/common/libs/cron.test.js @@ -0,0 +1,184 @@ +import moment from 'moment'; + +import { startOfDay, daysSince } from '../../../website/common/script/cron'; + +function localMoment (timeString, utcOffset) { + return moment(timeString).utcOffset(utcOffset, true); +} + +describe('cron utility functions', () => { + describe('startOfDay', () => { + it('is zero when no daystart configured', () => { + const options = { now: moment('2020-02-02 09:30:00Z'), timezoneOffset: 0 }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-02 00:00:00Z'); + }); + + it('is zero when negative daystart configured', () => { + const options = { + now: moment('2020-02-02 09:30:00Z'), + timezoneOffset: 0, + daystart: -5, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-02 00:00:00Z'); + }); + + it('is zero when daystart over 24 is configured', () => { + const options = { + now: moment('2020-02-02 09:30:00Z'), + timezoneOffset: 0, + daystart: 25, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-02 00:00:00Z'); + }); + + it('is equal to daystart o\'clock when daystart configured', () => { + const options = { + now: moment('2020-02-02 09:30:00Z'), + timezoneOffset: 0, + dayStart: 5, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-02 05:00:00Z'); + }); + + it('is previous day daystart o\'clock when daystart is after current time', () => { + const options = { + now: moment('2020-02-02 04:30:00Z'), + timezoneOffset: 0, + dayStart: 5, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-01 05:00:00Z'); + }); + + it('is daystart o\'clock when daystart is after current time due to timezone', () => { + const options = { + now: moment('2020-02-02 04:30:00Z'), + timezoneOffset: -120, + dayStart: 5, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-02 05:00:00+02:00'); + }); + + it('returns in default timezone if no timezone defined', () => { + const utcOffset = moment().utcOffset(); + const now = localMoment('2020-02-02 04:30:00', utcOffset).utc(); + + const result = startOfDay({ now }); + + expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset)); + }); + + it('returns in default timezone if timezone lower than -12:00', () => { + const utcOffset = moment().utcOffset(); + const options = { + now: localMoment('2020-02-02 17:30:00', utcOffset).utc(), + timezoneOffset: 721, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset)); + }); + + it('returns in default timezone if timezone higher than +14:00', () => { + const utcOffset = moment().utcOffset(); + const options = { + now: localMoment('2020-02-02 07:32:25.376', utcOffset).utc(), + timezoneOffset: -841, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset)); + }); + + it('returns in overridden timezone if override present', () => { + const options = { + now: moment('2020-02-02 13:30:27Z'), + timezoneOffset: 0, + timezoneUtcOffsetOverride: -240, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-02 00:00:00-04:00'); + }); + + it('returns start of yesterday if timezone difference carries it over datelines', () => { + const offset = 300; + const options = { + now: moment('2020-02-02 04:30:00Z'), + timezoneOffset: offset, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment(localMoment('2020-02-01', -offset)); + }); + }); + + describe('daysSince', () => { + it('correctly calculates days between two dates', () => { + const now = moment(); + const dayBeforeYesterday = moment(now).subtract({ days: 2 }); + + expect(daysSince(dayBeforeYesterday, { now })).to.equal(2); + }); + + it('is one lower if current time is before dayStart', () => { + const oneWeekAgoAtOnePm = moment().hour(13).subtract({ days: 7 }); + const thisMorningThreeAm = moment().hour(3); + const options = { + now: thisMorningThreeAm, + dayStart: 6, + }; + + const result = daysSince(oneWeekAgoAtOnePm, options); + + expect(result).to.equal(6); + }); + + it('is one higher if reference time is before dayStart and current time after dayStart', () => { + const oneWeekAgoAtEightAm = moment().hour(8).subtract({ days: 7 }); + const todayAtFivePm = moment().hour(17); + const options = { + now: todayAtFivePm, + dayStart: 11, + }; + + const result = daysSince(oneWeekAgoAtEightAm, options); + + expect(result).to.equal(8); + }); + + // Variations in timezone configuration options are already covered by startOfDay tests. + it('uses now in user timezone as configured in options', () => { + const timezoneOffset = 120; + const options = { + now: moment('1989-11-09 02:53:00+01:00'), + timezoneOffset, + }; + + const result = daysSince(localMoment('1989-11-08', -timezoneOffset), options); + + expect(result).to.equal(0); + }); + }); +}); diff --git a/test/common/libs/taskDefaults.test.js b/test/common/libs/taskDefaults.test.js index e69bafa45d..479a87fa30 100644 --- a/test/common/libs/taskDefaults.test.js +++ b/test/common/libs/taskDefaults.test.js @@ -1,6 +1,7 @@ import moment from 'moment'; import taskDefaults from '../../../website/common/script/libs/taskDefaults'; +import getUtcOffset from '../../../website/common/script/fns/getUtcOffset'; import { generateUser } from '../../helpers/common.helper'; describe('taskDefaults', () => { @@ -72,7 +73,7 @@ describe('taskDefaults', () => { expect(task.startDate).to.eql( moment() - .zone(user.preferences.timezoneOffset, 'hour') + .utcOffset(getUtcOffset(user)) .startOf('day') .subtract(1, 'day') .toDate(), diff --git a/test/common/shouldDo.test.js b/test/common/shouldDo.test.js index b8f9a604ad..d7250f871a 100644 --- a/test/common/shouldDo.test.js +++ b/test/common/shouldDo.test.js @@ -5,6 +5,8 @@ import 'moment-recur'; describe('shouldDo', () => { let day; let dailyTask; + // Options is a mapping of user.preferences, therefor `timezoneOffset` still holds old zone + // values instead of utcOffset values. let options = {}; let nextDue = []; @@ -80,17 +82,17 @@ describe('shouldDo', () => { it('returns true if the user\'s current time is after start date and Custom Day Start', () => { options.dayStart = 4; - day = moment().zone(options.timezoneOffset).startOf('day').add(6, 'hours') + day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(6, 'hours') .toDate(); - dailyTask.startDate = moment().zone(options.timezoneOffset).startOf('day').toDate(); + dailyTask.startDate = moment().utcOffset(-options.timezoneOffset).startOf('day').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); it('returns false if the user\'s current time is before Custom Day Start', () => { options.dayStart = 8; - day = moment().zone(options.timezoneOffset).startOf('day').add(2, 'hours') + day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(2, 'hours') .toDate(); - dailyTask.startDate = moment().zone(options.timezoneOffset).startOf('day').toDate(); + dailyTask.startDate = moment().utcOffset(-options.timezoneOffset).startOf('day').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(false); }); }); @@ -112,14 +114,14 @@ describe('shouldDo', () => { it('returns true if the user\'s current time is after Custom Day Start', () => { options.dayStart = 4; - day = moment().zone(options.timezoneOffset).startOf('day').add(6, 'hours') + day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(6, 'hours') .toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); it('returns false if the user\'s current time is before Custom Day Start', () => { options.dayStart = 8; - day = moment().zone(options.timezoneOffset).startOf('day').add(2, 'hours') + day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(2, 'hours') .toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(false); }); diff --git a/test/helpers/globals.helper.js b/test/helpers/globals.helper.js index bca96eac2c..213efac698 100644 --- a/test/helpers/globals.helper.js +++ b/test/helpers/globals.helper.js @@ -8,8 +8,9 @@ //------------------------------ global._ = require('lodash'); global.chai = require('chai'); -chai.use(require('sinon-chai')); chai.use(require('chai-as-promised')); +chai.use(require('chai-moment')); +chai.use(require('sinon-chai')); global.expect = chai.expect; global.sinon = require('sinon'); diff --git a/website/client/src/app.vue b/website/client/src/app.vue index 08c79f349a..21d00e8c93 100644 --- a/website/client/src/app.vue +++ b/website/client/src/app.vue @@ -297,7 +297,7 @@ export default { }; }, computed: { - ...mapState(['isUserLoggedIn', 'browserTimezoneOffset', 'isUserLoaded', 'notificationsRemoved']), + ...mapState(['isUserLoggedIn', 'browserTimezoneUtcOffset', 'isUserLoaded', 'notificationsRemoved']), ...mapState({ user: 'user.data' }), isStaticPage () { return this.$route.meta.requiresLogin === false; @@ -493,9 +493,10 @@ export default { this.hideLoadingScreen(); // Adjust the timezone offset - if (this.user.preferences.timezoneOffset !== this.browserTimezoneOffset) { + const browserTimezoneOffset = -this.browserTimezoneUtcOffset; + if (this.user.preferences.timezoneOffset !== browserTimezoneOffset) { this.$store.dispatch('user:set', { - 'preferences.timezoneOffset': this.browserTimezoneOffset, + 'preferences.timezoneOffset': browserTimezoneOffset, }); } diff --git a/website/client/src/components/settings/site.vue b/website/client/src/components/settings/site.vue index 6abe0d5279..cdd8ee79b2 100644 --- a/website/client/src/components/settings/site.vue +++ b/website/client/src/components/settings/site.vue @@ -559,6 +559,7 @@ import resetModal from './resetModal'; import deleteModal from './deleteModal'; import { SUPPORTED_SOCIAL_NETWORKS } from '@/../../common/script/constants'; import changeClass from '@/../../common/script/ops/changeClass'; +import getUtcOffset from '@/../../common/script/fns/getUtcOffset'; import notificationsMixin from '../../mixins/notifications'; import sounds from '../../libs/sounds'; import { buildAppleAuthUrl } from '../../libs/auth'; @@ -616,17 +617,8 @@ export default { return ['off', ...this.content.audioThemes]; }, timezoneOffsetToUtc () { - let offset = this.user.preferences.timezoneOffset; - const sign = offset > 0 ? '-' : '+'; - - offset = Math.abs(offset) / 60; - - const hour = Math.floor(offset); - - const minutesInt = (offset - hour) * 60; - const minutes = minutesInt < 10 ? `0${minutesInt}` : minutesInt; - - return `UTC${sign}${hour}:${minutes}`; + const offsetString = moment().utcOffset(getUtcOffset(this.user)).format('Z'); + return `UTC${offsetString}`; }, dayStart () { return this.user.preferences.dayStart; diff --git a/website/client/src/libs/auth.js b/website/client/src/libs/auth.js index 917812f545..41ae06de45 100644 --- a/website/client/src/libs/auth.js +++ b/website/client/src/libs/auth.js @@ -8,13 +8,14 @@ export function setUpAxios (AUTH_SETTINGS) { // eslint-disable-line import/prefe AUTH_SETTINGS = JSON.parse(AUTH_SETTINGS); // eslint-disable-line no-param-reassign } - const browserTimezoneOffset = moment().zone(); + const browserTimezoneUtcOffset = moment().utcOffset(); if (AUTH_SETTINGS.auth && AUTH_SETTINGS.auth.apiId && AUTH_SETTINGS.auth.apiToken) { axios.defaults.headers.common['x-api-user'] = AUTH_SETTINGS.auth.apiId; axios.defaults.headers.common['x-api-key'] = AUTH_SETTINGS.auth.apiToken; - axios.defaults.headers.common['x-user-timezoneOffset'] = browserTimezoneOffset; + // Communicate in "old" timezone variant for backwards compatibility + axios.defaults.headers.common['x-user-timezoneOffset'] = -browserTimezoneUtcOffset; return true; } diff --git a/website/client/src/store/index.js b/website/client/src/store/index.js index 94405ae54f..8723b10ff7 100644 --- a/website/client/src/store/index.js +++ b/website/client/src/store/index.js @@ -17,8 +17,8 @@ const IS_TEST = process.env.NODE_ENV === 'test'; // eslint-disable-line no-proce // before trying to load data let isUserLoggedIn = false; -// eg, 240 - this will be converted on server as -(offset/60) -const browserTimezoneOffset = moment().zone(); +// eg, -240 - this will be converted on server as (offset/60) +const browserTimezoneUtcOffset = moment().utcOffset(); axios.defaults.headers.common['x-client'] = 'habitica-web'; @@ -71,7 +71,7 @@ export default function () { // store the timezone offset in case it's different than the one in // user.preferences.timezoneOffset and change it after the user is synced // in app.vue - browserTimezoneOffset, + browserTimezoneUtcOffset, tasks: asyncResourceFactory(), // user tasks // @TODO use asyncresource? completedTodosStatus: 'NOT_LOADED', diff --git a/website/common/script/cron.js b/website/common/script/cron.js index 8afca4bcda..1342e6b8d2 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -33,26 +33,29 @@ function sanitizeOptions (o) { const ref = Number(o.dayStart || 0); const dayStart = !Number.isNaN(ref) && ref >= 0 && ref <= 24 ? ref : 0; - let timezoneOffset; - const timezoneOffsetDefault = Number(moment().zone()); + let timezoneUtcOffset; + const timezoneUtcOffsetDefault = moment().utcOffset(); - if (Number.isFinite(o.timezoneOffsetOverride)) { - timezoneOffset = Number(o.timezoneOffsetOverride); + if (Number.isFinite(o.timezoneUtcOffset)) { + // Options were already sanitized + timezoneUtcOffset = o.timezoneUtcOffset; + } else if (Number.isFinite(o.timezoneUtcOffsetOverride)) { + timezoneUtcOffset = o.timezoneUtcOffsetOverride; } else if (Number.isFinite(o.timezoneOffset)) { - timezoneOffset = Number(o.timezoneOffset); + timezoneUtcOffset = -o.timezoneOffset; } else { - timezoneOffset = timezoneOffsetDefault; + timezoneUtcOffset = timezoneUtcOffsetDefault; } - if (timezoneOffset > 720 || timezoneOffset < -840) { - // timezones range from -12 (offset +720) to +14 (offset -840) - timezoneOffset = timezoneOffsetDefault; + if (timezoneUtcOffset < -720 || timezoneUtcOffset > 840) { + // timezones range from -12 (offset -720) to +14 (offset 840) + timezoneUtcOffset = timezoneUtcOffsetDefault; } - const now = o.now ? moment(o.now).zone(timezoneOffset) : moment().zone(timezoneOffset); + const now = moment(o.now).utcOffset(timezoneUtcOffset); // return a new object, we don't want to add "now" to user object return { dayStart, - timezoneOffset, + timezoneUtcOffset, now, }; } @@ -81,7 +84,7 @@ export function startOfDay (options = {}) { const o = sanitizeOptions(options); const dayStart = moment(o.now).startOf('day').add({ hours: o.dayStart }); - if (moment(o.now).hour() < o.dayStart) { + if (o.now.hour() < o.dayStart) { dayStart.subtract({ days: 1 }); } @@ -119,7 +122,7 @@ export function shouldDo (day, dailyTask, options = {}) { // NB: The user's day start date has already been converted to the PREVIOUS // day's date if the time portion was before CDS. - const startDate = moment(dailyTask.startDate).zone(o.timezoneOffset).startOf('day'); + const startDate = moment(dailyTask.startDate).utcOffset(o.timezoneUtcOffset).startOf('day'); if (startDate > startOfDayWithCDSTime.startOf('day') && !options.nextDue) { return false; // Daily starts in the future diff --git a/website/common/script/fns/getUtcOffset.js b/website/common/script/fns/getUtcOffset.js new file mode 100644 index 0000000000..7f17f9a363 --- /dev/null +++ b/website/common/script/fns/getUtcOffset.js @@ -0,0 +1,12 @@ +/** + * Converts from timezoneOffset (which corresponded to the value returned + * from moment.js's deprecated `moment#zone` method) to timezoneUtcOffset. + * + * This is done with conversion instead of changing it in the database to + * be backwards compatible with the database values and old clients. + * + * Not as a user method since it needs to work in the frontend as well. + */ +export default function getUtcOffset (user) { + return -(user.preferences.timezoneOffset || 0); +} diff --git a/website/common/script/index.js b/website/common/script/index.js index b2435c42d3..d3e05c26af 100644 --- a/website/common/script/index.js +++ b/website/common/script/index.js @@ -28,6 +28,7 @@ import apiErrors from './errors/apiErrorMessages'; import commonErrors from './errors/commonErrorMessages'; import autoAllocate from './fns/autoAllocate'; import crit from './fns/crit'; +import getUtcOffset from './fns/getUtcOffset'; import handleTwoHanded from './fns/handleTwoHanded'; import predictableRandom from './fns/predictableRandom'; import randomDrop from './fns/randomDrop'; @@ -151,6 +152,7 @@ api.fns = { resetGear, ultimateGear, updateStats, + getUtcOffset, }; api.ops = { diff --git a/website/common/script/libs/taskDefaults.js b/website/common/script/libs/taskDefaults.js index 69ba53824b..d966626e0b 100644 --- a/website/common/script/libs/taskDefaults.js +++ b/website/common/script/libs/taskDefaults.js @@ -1,6 +1,7 @@ import { v4 as uuid } from 'uuid'; import defaults from 'lodash/defaults'; import moment from 'moment'; +import getUtcOffset from '../fns/getUtcOffset'; // Even though Mongoose handles task defaults, // we want to make sure defaults are set on the client-side before @@ -66,7 +67,7 @@ export default function taskDefaults (task, user) { } if (task.type === 'daily') { - const now = moment().zone(user.preferences.timezoneOffset); + const now = moment().utcOffset(getUtcOffset(user)); const startOfDay = now.clone().startOf('day'); const startOfDayWithCDSTime = startOfDay .clone() diff --git a/website/common/script/ops/scoreTask.js b/website/common/script/ops/scoreTask.js index 1062d901e4..4bd3be7cae 100644 --- a/website/common/script/ops/scoreTask.js +++ b/website/common/script/ops/scoreTask.js @@ -8,6 +8,8 @@ import { import i18n from '../i18n'; import updateStats from '../fns/updateStats'; import crit from '../fns/crit'; +import getUtcOffset from '../fns/getUtcOffset'; + import statsComputed from '../libs/statsComputed'; import { checkOnboardingStatus } from '../libs/onboarding'; @@ -194,14 +196,14 @@ function _lastHistoryEntryWasToday (lastHistoryEntry, user) { return false; } - const { timezoneOffset } = user.preferences; + const timezoneUtcOffset = getUtcOffset(user); const { dayStart } = user.preferences; // Adjust the last entry date according to the user's timezone and CDS - const dateWithTimeZone = moment(lastHistoryEntry.date).zone(timezoneOffset); + const dateWithTimeZone = moment(lastHistoryEntry.date).utcOffset(timezoneUtcOffset); if (dateWithTimeZone.hour() < dayStart) dateWithTimeZone.subtract(1, 'day'); - return moment().zone(timezoneOffset).isSame(dateWithTimeZone, 'day'); + return moment().utcOffset(timezoneUtcOffset).isSame(dateWithTimeZone, 'day'); } function _updateLastHistoryEntry (lastHistoryEntry, task, direction, times) { diff --git a/website/server/controllers/top-level/dataexport.js b/website/server/controllers/top-level/dataexport.js index 9de16046ed..2a17f4e5e4 100644 --- a/website/server/controllers/top-level/dataexport.js +++ b/website/server/controllers/top-level/dataexport.js @@ -317,7 +317,7 @@ api.exportUserPrivateMessages = { async handler (req, res) { const { user } = res.locals; - const { timezoneOffset } = user.preferences; + const timezoneUtcOffset = user.getUtcOffset(); const dateFormat = user.preferences.dateFormat.toUpperCase(); const TO = res.t('to'); const FROM = res.t('from'); @@ -329,7 +329,7 @@ api.exportUserPrivateMessages = { inbox.forEach((message, index) => { const recipientLabel = message.sent ? TO : FROM; const messageUser = message.user; - const timestamp = moment.utc(message.timestamp).zone(timezoneOffset).format(`${dateFormat} HH:mm:ss`); + const timestamp = moment.utc(message.timestamp).utcOffset(timezoneUtcOffset).format(`${dateFormat} HH:mm:ss`); const text = md.render(message.text); const pageIndex = `(${index + 1}/${inbox.length})`; messages += ` diff --git a/website/server/libs/cron.js b/website/server/libs/cron.js index 2c9d266bc8..774309ecb5 100644 --- a/website/server/libs/cron.js +++ b/website/server/libs/cron.js @@ -172,7 +172,7 @@ function resetHabitCounters (user, tasksByType, now, daysMissed) { break; } const thatDay = moment(now) - .zone(user.preferences.timezoneOffset + user.preferences.dayStart * 60) + .utcOffset(user.getUtcOffset() - user.preferences.dayStart * 60) .subtract({ days: i }); if (thatDay.day() === 1) { resetWeekly = true; @@ -281,14 +281,14 @@ function awardLoginIncentives (user) { // Perform various beginning-of-day reset actions. export function cron (options = {}) { const { - user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs, + user, tasksByType, analytics, now = new Date(), daysMissed, timezoneUtcOffsetFromUserPrefs, } = options; let _progress = { down: 0, up: 0, collectedItems: 0 }; // Record pre-cron values of HP and MP to show notifications later const beforeCronStats = _.pick(user.stats, ['hp', 'mp']); - user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs; + user.preferences.timezoneOffsetAtLastCron = -timezoneUtcOffsetFromUserPrefs; // User is only allowed a certain number of drops a day. This resets the count. if (user.items.lastDrop.count > 0) user.items.lastDrop.count = 0; diff --git a/website/server/libs/preening.js b/website/server/libs/preening.js index a6a4bb64e7..30228e2d29 100644 --- a/website/server/libs/preening.js +++ b/website/server/libs/preening.js @@ -2,10 +2,10 @@ import _ from 'lodash'; import moment from 'moment'; // Aggregate entries -function _aggregate (history, aggregateBy, timezoneOffset, dayStart) { +function _aggregate (history, aggregateBy, timezoneUtcOffset, dayStart) { return _.chain(history) .groupBy(entry => { // group entries by aggregateBy - const entryDate = moment(entry.date).zone(timezoneOffset); + const entryDate = moment(entry.date).utcOffset(timezoneUtcOffset); if (entryDate.hour() < dayStart) entryDate.subtract(1, 'day'); return entryDate.format(aggregateBy); }) @@ -35,16 +35,16 @@ Subscribers and challenges: - 1 value each month for the previous 12 months - 1 value each year for the previous years */ -export function preenHistory (history, isSubscribed, timezoneOffset = 0, dayStart = 0) { +export function preenHistory (history, isSubscribed, timezoneUtcOffset = 0, dayStart = 0) { // history = _.filter(history, historyEntry => Boolean(historyEntry)); // Filter missing entries - const now = moment().zone(timezoneOffset); + const now = moment().utcOffset(timezoneUtcOffset); // Date after which to begin compressing data const cutOff = now.subtract(isSubscribed ? 365 : 60, 'days').startOf('day'); // Keep uncompressed entries (modifies history and returns removed items) const newHistory = _.remove(history, entry => { if (!entry) return true; // sometimes entries are `null` - const entryDate = moment(entry.date).zone(timezoneOffset); + const entryDate = moment(entry.date).utcOffset(timezoneUtcOffset); if (entryDate.hour() < dayStart) entryDate.subtract(1, 'day'); return entryDate.isSame(cutOff) || entryDate.isAfter(cutOff); }); @@ -53,13 +53,13 @@ export function preenHistory (history, isSubscribed, timezoneOffset = 0, dayStar const monthsCutOff = cutOff.subtract(isSubscribed ? 12 : 10, 'months').startOf('day'); const aggregateByMonth = _.remove(history, entry => { if (!entry) return true; // sometimes entries are `null` - const entryDate = moment(entry.date).zone(timezoneOffset); + const entryDate = moment(entry.date).utcOffset(timezoneUtcOffset); if (entryDate.hour() < dayStart) entryDate.subtract(1, 'day'); return entryDate.isSame(monthsCutOff) || entryDate.isAfter(monthsCutOff); }); // Aggregate remaining entries by month and year - if (aggregateByMonth.length > 0) newHistory.unshift(..._aggregate(aggregateByMonth, 'YYYYMM', timezoneOffset, dayStart)); - if (history.length > 0) newHistory.unshift(..._aggregate(history, 'YYYY', timezoneOffset, dayStart)); + if (aggregateByMonth.length > 0) newHistory.unshift(..._aggregate(aggregateByMonth, 'YYYYMM', timezoneUtcOffset, dayStart)); + if (history.length > 0) newHistory.unshift(..._aggregate(history, 'YYYY', timezoneUtcOffset, dayStart)); return newHistory; } @@ -67,13 +67,13 @@ export function preenHistory (history, isSubscribed, timezoneOffset = 0, dayStar // Preen history for users and tasks. export function preenUserHistory (user, tasksByType) { const isSubscribed = user.isSubscribed(); - const { timezoneOffset } = user.preferences; + const timezoneUtcOffset = user.getUtcOffset(); const { dayStart } = user.preferences; const minHistoryLength = isSubscribed ? 365 : 60; function _processTask (task) { if (task.history && task.history.length > minHistoryLength) { - task.history = preenHistory(task.history, isSubscribed, timezoneOffset, dayStart); + task.history = preenHistory(task.history, isSubscribed, timezoneUtcOffset, dayStart); task.markModified('history'); } } @@ -82,12 +82,14 @@ export function preenUserHistory (user, tasksByType) { tasksByType.dailys.forEach(_processTask); if (user.history.exp.length > minHistoryLength) { - user.history.exp = preenHistory(user.history.exp, isSubscribed, timezoneOffset, dayStart); + user.history.exp = preenHistory(user.history.exp, isSubscribed, timezoneUtcOffset, dayStart); user.markModified('history.exp'); } if (user.history.todos.length > minHistoryLength) { - user.history.todos = preenHistory(user.history.todos, isSubscribed, timezoneOffset, dayStart); + user.history.todos = preenHistory( + user.history.todos, isSubscribed, timezoneUtcOffset, dayStart, + ); user.markModified('history.todos'); } } diff --git a/website/server/middlewares/cron.js b/website/server/middlewares/cron.js index 365acaefd6..62e1e85a68 100644 --- a/website/server/middlewares/cron.js +++ b/website/server/middlewares/cron.js @@ -64,7 +64,7 @@ async function cronAsync (req, res) { user = await User.findOne({ _id: user._id }).exec(); res.locals.user = user; - const { daysMissed, timezoneOffsetFromUserPrefs } = user.daysUserHasMissed(now, req); + const { daysMissed, timezoneUtcOffsetFromUserPrefs } = user.daysUserHasMissed(now, req); await updateLastCron(user, now); @@ -94,7 +94,7 @@ async function cronAsync (req, res) { now, daysMissed, analytics, - timezoneOffsetFromUserPrefs, + timezoneUtcOffsetFromUserPrefs, headers: req.headers, }); diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js index 4bbe05decc..9c7d0d9a7c 100644 --- a/website/server/models/user/methods.js +++ b/website/server/models/user/methods.js @@ -347,35 +347,40 @@ schema.methods.cancelSubscription = async function cancelSubscription (options = return payments.cancelSubscription(options); }; +schema.methods.getUtcOffset = function getUtcOffset () { + return common.fns.getUtcOffset(this); +}; + schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) { // If the user's timezone has changed (due to travel or daylight savings), // cron can be triggered twice in one day, so we check for that and use // both timezones to work out if cron should run. // CDS = Custom Day Start time. - let timezoneOffsetFromUserPrefs = this.preferences.timezoneOffset; - const timezoneOffsetAtLastCron = Number.isFinite(this.preferences.timezoneOffsetAtLastCron) - ? this.preferences.timezoneOffsetAtLastCron - : timezoneOffsetFromUserPrefs; - let timezoneOffsetFromBrowser = typeof req.header === 'function' && Number(req.header('x-user-timezoneoffset')); - timezoneOffsetFromBrowser = Number.isFinite(timezoneOffsetFromBrowser) - ? timezoneOffsetFromBrowser - : timezoneOffsetFromUserPrefs; + let timezoneUtcOffsetFromUserPrefs = this.getUtcOffset(); + const timezoneUtcOffsetAtLastCron = Number.isFinite(this.preferences.timezoneOffsetAtLastCron) + ? -this.preferences.timezoneOffsetAtLastCron + : timezoneUtcOffsetFromUserPrefs; + + let timezoneUtcOffsetFromBrowser = typeof req.header === 'function' && -Number(req.header('x-user-timezoneoffset')); + timezoneUtcOffsetFromBrowser = Number.isFinite(timezoneUtcOffsetFromBrowser) + ? timezoneUtcOffsetFromBrowser + : timezoneUtcOffsetFromUserPrefs; // NB: All timezone offsets can be 0, so can't use `... || ...` to apply non-zero defaults - if (timezoneOffsetFromBrowser !== timezoneOffsetFromUserPrefs) { + if (timezoneUtcOffsetFromBrowser !== timezoneUtcOffsetFromUserPrefs) { // The user's browser has just told Habitica that the user's timezone has // changed so store and use the new zone. - this.preferences.timezoneOffset = timezoneOffsetFromBrowser; - timezoneOffsetFromUserPrefs = timezoneOffsetFromBrowser; + this.preferences.timezoneOffset = -timezoneUtcOffsetFromBrowser; + timezoneUtcOffsetFromUserPrefs = timezoneUtcOffsetFromBrowser; } // How many days have we missed using the user's current timezone: let daysMissed = daysSince(this.lastCron, defaults({ now }, this.preferences)); - if (timezoneOffsetAtLastCron !== timezoneOffsetFromUserPrefs) { + if (timezoneUtcOffsetAtLastCron !== timezoneUtcOffsetFromUserPrefs) { // Give the user extra time based on the difference in timezones - if (timezoneOffsetAtLastCron < timezoneOffsetFromUserPrefs) { - const differenceBetweenTimezonesInMinutes = timezoneOffsetFromUserPrefs - timezoneOffsetAtLastCron; // eslint-disable-line max-len + if (timezoneUtcOffsetAtLastCron > timezoneUtcOffsetFromUserPrefs) { + const differenceBetweenTimezonesInMinutes = timezoneUtcOffsetAtLastCron - timezoneUtcOffsetFromUserPrefs; // eslint-disable-line max-len now = moment(now).subtract(differenceBetweenTimezonesInMinutes, 'minutes'); // eslint-disable-line no-param-reassign, max-len } @@ -384,13 +389,13 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) { const daysMissedNewZone = daysMissed; const daysMissedOldZone = daysSince(this.lastCron, defaults({ now, - timezoneOffsetOverride: timezoneOffsetAtLastCron, + timezoneUtcOffsetOverride: timezoneUtcOffsetAtLastCron, }, this.preferences)); - if (timezoneOffsetAtLastCron < timezoneOffsetFromUserPrefs) { + if (timezoneUtcOffsetAtLastCron > timezoneUtcOffsetFromUserPrefs) { // The timezone change was in the unsafe direction. - // E.g., timezone changes from UTC+1 (offset -60) to UTC+0 (offset 0). - // or timezone changes from UTC-4 (offset 240) to UTC-5 (offset 300). + // E.g., timezone changes from UTC+1 (utcOffset 60) to UTC+0 (offset 0). + // or timezone changes from UTC-4 (utcOffset -240) to UTC-5 (utcOffset -300). // Local time changed from, for example, 03:00 to 02:00. if (daysMissedOldZone > 0 && daysMissedNewZone > 0) { @@ -419,20 +424,21 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) { // timezone interprets as being in today. daysMissed = 0; // prevent cron running now - const timezoneOffsetDiff = timezoneOffsetAtLastCron - timezoneOffsetFromUserPrefs; - // e.g., for dangerous zone change: 240 - 300 = -60 or -660 - -600 = -60 + const timezoneOffsetDiff = timezoneUtcOffsetFromUserPrefs - timezoneUtcOffsetAtLastCron; + // e.g., for dangerous zone change: -300 - -240 = -60 or 600 - 660= -60 this.lastCron = moment(this.lastCron).subtract(timezoneOffsetDiff, 'minutes'); // NB: We don't change this.auth.timestamps.loggedin so that will still record // the time that the previous cron actually ran. // From now on we can ignore the old timezone: - this.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs; + // This is still timezoneOffset for backwards compatibility reasons. + this.preferences.timezoneOffsetAtLastCron = -timezoneUtcOffsetAtLastCron; } else { // Both old and new timezones indicate that cron should // NOT run. daysMissed = 0; // prevent cron running now } - } else if (timezoneOffsetAtLastCron > timezoneOffsetFromUserPrefs) { + } else if (timezoneUtcOffsetAtLastCron < timezoneUtcOffsetFromUserPrefs) { daysMissed = daysMissedNewZone; // TODO: Either confirm that there is nothing that could possibly go wrong // here and remove the need for this else branch, or fix stuff. @@ -445,7 +451,7 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) { } } - return { daysMissed, timezoneOffsetFromUserPrefs }; + return { daysMissed, timezoneUtcOffsetFromUserPrefs }; }; async function getUserGroupData (user) { From 7ee6ff18ce27cf55ecd80039c381f9f55c397f05 Mon Sep 17 00:00:00 2001 From: Jake North <43115570+jakenorth@users.noreply.github.com> Date: Sat, 25 Jul 2020 04:46:06 -0700 Subject: [PATCH 04/12] Fix height of badges in multiline achievements (#12406) This fixes the UI bug I reported where achievement names taking up multiple lines cause badges to stretch to fill their container. Screenshot of bug: https://i.snipboard.io/07GBi4.jpg Screenshot of fix: https://i.snipboard.io/PKvi8e.jpg --- website/client/src/components/userMenu/profile.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/website/client/src/components/userMenu/profile.vue b/website/client/src/components/userMenu/profile.vue index 6eed23f91e..e95073b41e 100644 --- a/website/client/src/components/userMenu/profile.vue +++ b/website/client/src/components/userMenu/profile.vue @@ -618,6 +618,7 @@ margin-right: 8px; background: $gray-600; color: $gray-300; + height: fit-content; } } } From aaf32cc09b3f46c144e912bb94b6e7e1d76a79d5 Mon Sep 17 00:00:00 2001 From: negue Date: Sat, 25 Jul 2020 14:37:10 +0200 Subject: [PATCH 05/12] Teams UI Redesign and A11y Updates (#12142) * WIP(a11y): task modal updates * fix(tasks): borders in modal * fix(tasks): circley locks * fix(task-modal): placeholders * WIP(task-modal): disabled states, hide empty options, +/- restyle * fix(task-modal): box shadows instead of borders, habit control pointer * fix(task-modal): button states? * fix(modal): tighten up layout, new spacing utils * fix(tasks): more stylin * fix(tasks): habit hovers * fix(css): checklist labels, a11y colors * fix(css): one more missed hover issue * fix(css): lock Challenges, label fixes * fix(css): scope input/textarea changes * fix(style): task tweakies * fix(style): more button fixage * WIP(component): start select list story * working example of a templated selectList * fix(style): more button corrections * fix(lint): EOL * fix(buttons): factor btn-secondary to better override Bootstrap * fix(styles): standardize more buttons * wip: difficulty select - style fixes * selectDifficulty works! :tada: - fix styles * change the dropdown-item sizes only for the selectList ones * selectTranslatedArray * changed many label margins * more correct dropdown style * fix(modals): button corrections * input-group styling + datetime picker without today button * Style/margins for "repeat every" - extract selectTag.vue * working tag-selection / update - cleanup * fix stories * fix svg color on create modal (purple) * fix task modal bottom padding * correct dropdown shadow * update dropdown-toggle caret size / color * fixed checklist style * sync checked state * selectTag padding * fix spacing between positive/negative streak inputs * toggle-checkbox + fix some spacings * disable repeat-on when its a groupTask * fix new checklist-item * fix toggle-checkbox style - fix difficulty style * fix checklist ui * add tags label , when there arent any tags selected * WORKING select-tag component :tada: * fix taglist story * show max 5 items in tag dropdown + "X more" label * fix datetime clear button * replace m-b-xs to mb-1 (bootstrap) - fix input-group-text style * fix styles of advanced settings * fix delete task styles * always show grippy on hover of the item * extract modal-text-input mixin + fix the borders/dropshadow * fix(spacing): revert most to Bootstrap * feat(checklists): make local copy of master checklist non-editable also aggressively update checklists because they weren't syncing?? * fix(checklists): handle add/remove options better * feat(teams): manager notes field * fix select/dropdown styles * input border + icon colors * delete task underline color * fix checklist "delete icon" vertical position * selectTag fixes - normal open/close toggle working again - remove icon color * fixing icons: Trash can - Delete Little X - Remove Big X - Close Block - Block * fix taglist margins / icon sizes * wip margin overview (in storybook) * fix routerlink * remove unused method * new selectTag style + add markdown inside tagList + scrollable tag selection * fix selectTag / selectList active border * fix difficulty select (svg default color) * fix input padding-left + fix reset habit streak fullwidth / padding + "repeat every" gray text (no border) * feat(teams): improved approval request > approve > reward flow * fix(tests): address failures * fix(lint): oops only * fix(tasks): short-circuit group related logic * fix(tasks): more short circuiting * fix(tasks): more lines, less lint * fix(tasks): how do i keep missing these * feat(teams): provide assigning user summary * fix(teams): don't attempt to record assiging user if not supplied * fix advanced-settings styling / margin * fix merge + hide advanced streak settings when none enabled * fix styles * set Roboto font for advanced settings * Add Challenge flag to the tag list * add tag with enter, when no other tag is found * fix styles + tag cancel button * refactor footer / margin * split repeat fields into option mt-3 groups * button all the things * fix(tasks): style updates * no hover state for non-editable tasks on team board * keep assign/claim footer on task after requesting approval * disable more fields on user copy of team task, and remove hover states for them * fix(tasks): functional revisions * "Claim Rewards" instead of "x" in task approved notif * Remove default transition supplied by Bootstrap, apply individually to some elements * Delete individual tasks and related notifications when master task deleted from team board * Manager notes now save when supplied at task initial creation * Can no longer dismiss rewards from approved task by hitting Dismiss All * fix(tasks): clean tasksOrder also adjust related test expectation * fix(tests): adjust integration expectations * fix(test): ratzen fratzen only * fix(teams): checklist, notes * fix(teams): improve disabled states * fix(teams): more style fixage * BREAKING(teams): return 202 instead of 401 for approval request * fix(teams): better taskboard sync also re-re-fix checklist borders * fix(tests): update expectations for breaking change * refactor(task-modal): lockable label component * refactor(teams): move task scoring to mixin * fix(teams): style corrections * fix(tasks): spacing and wording corrections * fix(teams): don't bork manager notes * fix(teams): assignment fix and more approval flow revisions * WIP(teams): use tag dropdown control for assignment * refactor(tasks): better spacing, generic multi select * fix(tasks): various visual and behavior updates * fix(tasks): incidental style tweaks * fix(teams): standardize approval request response * refactor(teams): correct test, use res.respond message param * fix(storybook): renamed component * fix(teams): age approval-required To Do's Fixes #8730 * fix(teams): sync personal data as well as team on mixin sync * fix(teams): hide unclaim button, not whole footer; fix switch focus * fix(achievements): unrevert width fix Co-authored-by: Sabe Jones --- test/api/unit/models/group_tasks.test.js | 7 +- .../groups/POST-group_remove_manager.test.js | 7 +- .../groups/DELETE-group_tasks_id.test.js | 33 +- ...POST-group_tasks_id_approve_userId.test.js | 79 +- ...T-group_tasks_id_needs-work_userId.test.js | 23 +- ...OST-group_tasks_id_score_direction.test.js | 32 +- .../tasks/groups/PUT-group_task_id.test.js | 10 +- website/client/config/storybook/config.js | 2 + website/client/config/storybook/margin.css | 13 + website/client/src/assets/scss/button.scss | 8 +- website/client/src/assets/scss/dropdown.scss | 43 +- website/client/src/assets/scss/form.scss | 92 +- website/client/src/assets/scss/icon.scss | 10 +- website/client/src/assets/scss/index.scss | 1 + website/client/src/assets/scss/misc.scss | 14 +- website/client/src/assets/scss/modal.scss | 1 + website/client/src/assets/scss/spacing.scss | 59 + website/client/src/assets/scss/task.scss | 33 +- .../client/src/assets/scss/typography.scss | 6 +- website/client/src/assets/svg/information.svg | 2 +- .../src/components/achievements/newStuff.vue | 8 +- website/client/src/components/appFooter.vue | 5 +- .../group-plans/taskInformation.vue | 21 +- .../components/groups/communityGuidelines.vue | 2 +- .../notifications/groupTaskApproval.vue | 9 +- .../notifications/groupTaskApproved.vue | 31 +- .../header/notificationsDropdown.vue | 2 +- .../src/components/header/userDropdown.vue | 20 +- .../client/src/components/inventory/item.vue | 4 +- .../components/inventory/stable/foodItem.vue | 2 +- .../components/inventory/stable/mountItem.vue | 2 +- .../components/inventory/stable/petItem.vue | 2 +- .../client/src/components/notifications.vue | 51 +- .../src/components/payments/sendGemsModal.vue | 62 +- .../client/src/components/settings/api.vue | 14 +- .../src/components/snackbars/notification.vue | 2 +- .../client/src/components/static/header.vue | 2 +- website/client/src/components/static/home.vue | 11 +- .../src/components/static/staticWrapper.vue | 2 +- .../src/components/tasks/approvalFooter.vue | 19 +- .../src/components/tasks/approvalHeader.vue | 16 +- .../client/src/components/tasks/column.vue | 1 + .../tasks/modal-controls/checklist.vue | 59 +- .../tasks/modal-controls/lockableLabel.vue | 65 + .../tasks/modal-controls/multiList.vue | 187 +++ .../tasks/modal-controls/selectDifficulty.vue | 182 +++ .../modal-controls/selectMulti.stories.js | 133 ++ .../tasks/modal-controls/selectMulti.vue | 294 +++++ .../modal-controls/selectTranslatedArray.vue | 74 ++ .../client/src/components/tasks/tagsPopup.vue | 187 --- website/client/src/components/tasks/task.vue | 198 +-- .../client/src/components/tasks/taskModal.vue | 1158 ++++++++--------- .../src/components/ui/checkbox.stories.js | 70 + website/client/src/components/ui/checkbox.vue | 14 +- .../src/components/ui/input-group.stories.js | 71 + .../src/components/ui/margin.stories.js | 45 + .../src/components/ui/selectList.stories.js | 110 ++ .../client/src/components/ui/selectList.vue | 68 + .../src/components/ui/toggleCheckbox.vue | 116 ++ .../client/src/components/ui/toggleSwitch.vue | 31 +- .../src/components/userMenu/profile.vue | 35 +- website/client/src/mixins/scoreTask.js | 136 ++ website/client/src/mixins/sync.js | 3 + website/client/src/store/actions/tags.js | 8 +- website/client/src/store/getters/tasks.js | 17 +- website/common/locales/en/groups.json | 13 +- website/common/locales/en/tasks.json | 5 +- website/common/script/ops/scoreTask.js | 2 +- website/server/controllers/api-v3/tasks.js | 24 +- .../server/controllers/api-v3/tasks/groups.js | 10 +- website/server/libs/taskManager.js | 1 + website/server/models/group.js | 79 +- website/server/models/task.js | 20 +- 73 files changed, 2769 insertions(+), 1409 deletions(-) create mode 100644 website/client/config/storybook/margin.css create mode 100644 website/client/src/assets/scss/spacing.scss create mode 100644 website/client/src/components/tasks/modal-controls/lockableLabel.vue create mode 100644 website/client/src/components/tasks/modal-controls/multiList.vue create mode 100644 website/client/src/components/tasks/modal-controls/selectDifficulty.vue create mode 100644 website/client/src/components/tasks/modal-controls/selectMulti.stories.js create mode 100644 website/client/src/components/tasks/modal-controls/selectMulti.vue create mode 100644 website/client/src/components/tasks/modal-controls/selectTranslatedArray.vue delete mode 100644 website/client/src/components/tasks/tagsPopup.vue create mode 100644 website/client/src/components/ui/checkbox.stories.js create mode 100644 website/client/src/components/ui/input-group.stories.js create mode 100644 website/client/src/components/ui/margin.stories.js create mode 100644 website/client/src/components/ui/selectList.stories.js create mode 100644 website/client/src/components/ui/selectList.vue create mode 100644 website/client/src/components/ui/toggleCheckbox.vue create mode 100644 website/client/src/mixins/scoreTask.js diff --git a/test/api/unit/models/group_tasks.test.js b/test/api/unit/models/group_tasks.test.js index 848b4b0792..1c60fa947b 100644 --- a/test/api/unit/models/group_tasks.test.js +++ b/test/api/unit/models/group_tasks.test.js @@ -235,15 +235,16 @@ describe('Group Task Methods', () => { }); }); - it('removes an assigned task and unlinks assignees', async () => { + it('removes assigned tasks when master task is deleted', async () => { await guild.syncTask(task, leader); await guild.removeTask(task); const updatedLeader = await User.findOne({ _id: leader._id }); - const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } }); + const updatedLeadersTasks = await Tasks.Task.find({ userId: leader._id, type: taskType }); const syncedTask = find(updatedLeadersTasks, findLinkedTask); - expect(syncedTask.group.broken).to.equal('TASK_DELETED'); + expect(updatedLeader.tasksOrder[`${taskType}s`]).to.not.include(task._id); + expect(syncedTask).to.not.exist; }); it('unlinks and deletes group tasks for a user when remove-all is specified', async () => { diff --git a/test/api/v3/integration/groups/POST-group_remove_manager.test.js b/test/api/v3/integration/groups/POST-group_remove_manager.test.js index e8cae4ab29..6c5d4cee9f 100644 --- a/test/api/v3/integration/groups/POST-group_remove_manager.test.js +++ b/test/api/v3/integration/groups/POST-group_remove_manager.test.js @@ -75,12 +75,7 @@ describe('POST /group/:groupId/remove-manager', () => { await nonLeader.post(`/tasks/${task._id}/assign/${nonManager._id}`); const memberTasks = await nonManager.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(nonManager.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await nonManager.post(`/tasks/${syncedTask._id}/score/up`); const updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, { managerId: nonLeader._id, diff --git a/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js b/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js index 59dba9f5ed..73828accdc 100644 --- a/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js +++ b/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js @@ -73,12 +73,7 @@ describe('Groups DELETE /tasks/:id', () => { }); const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.sync(); await member2.sync(); @@ -96,16 +91,16 @@ describe('Groups DELETE /tasks/:id', () => { expect(member2.notifications.length).to.equal(1); }); - it('unlinks assigned user', async () => { + it('deletes task from assigned user', async () => { await user.del(`/tasks/${task._id}`); const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - expect(syncedTask.group.broken).to.equal('TASK_DELETED'); + expect(syncedTask).to.not.exist; }); - it('unlinks all assigned users', async () => { + it('deletes task from all assigned users', async () => { await user.del(`/tasks/${task._id}`); const memberTasks = await member.get('/tasks/user'); @@ -114,8 +109,8 @@ describe('Groups DELETE /tasks/:id', () => { const member2Tasks = await member2.get('/tasks/user'); const member2SyncedTask = find(member2Tasks, findAssignedTask); - expect(syncedTask.group.broken).to.equal('TASK_DELETED'); - expect(member2SyncedTask.group.broken).to.equal('TASK_DELETED'); + expect(syncedTask).to.not.exist; + expect(member2SyncedTask).to.not.exist; }); it('prevents a user from deleting a task they are assigned to', async () => { @@ -130,22 +125,6 @@ describe('Groups DELETE /tasks/:id', () => { }); }); - it('allows a user to delete a broken task', async () => { - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - await user.del(`/tasks/${task._id}`); - - await member.del(`/tasks/${syncedTask._id}`); - - await expect(member.get(`/tasks/${syncedTask._id}`)) - .to.eventually.be.rejected.and.eql({ - code: 404, - error: 'NotFound', - message: 'Task not found.', - }); - }); - it('allows a user to delete a task after leaving a group', async () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js index 599086e463..1d23e5f85c 100644 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js @@ -58,22 +58,14 @@ describe('POST /tasks/:id/approve/:userId', () => { let memberTasks = await member.get('/tasks/user'); let syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.post(`/tasks/${task._id}/approve/${member._id}`); await member.sync(); - expect(member.notifications.length).to.equal(3); + expect(member.notifications.length).to.equal(2); expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED'); expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text })); - expect(member.notifications[2].type).to.equal('SCORED_TASK'); - expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text })); memberTasks = await member.get('/tasks/user'); syncedTask = find(memberTasks, findAssignedTask); @@ -93,21 +85,13 @@ describe('POST /tasks/:id/approve/:userId', () => { let memberTasks = await member.get('/tasks/user'); let syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await member2.post(`/tasks/${task._id}/approve/${member._id}`); await member.sync(); - expect(member.notifications.length).to.equal(3); + expect(member.notifications.length).to.equal(2); expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED'); expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text })); - expect(member.notifications[2].type).to.equal('SCORED_TASK'); - expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text })); memberTasks = await member.get('/tasks/user'); syncedTask = find(memberTasks, findAssignedTask); @@ -125,12 +109,7 @@ describe('POST /tasks/:id/approve/:userId', () => { await member2.post(`/tasks/${task._id}/assign/${member._id}`); const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.sync(); await member2.sync(); @@ -157,14 +136,9 @@ describe('POST /tasks/:id/approve/:userId', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await member2.post(`/tasks/${task._id}/approve/${member._id}`); + await expect(user.post(`/tasks/${task._id}/approve/${member._id}`)) .to.eventually.be.rejected.and.to.eql({ code: 401, @@ -197,13 +171,7 @@ describe('POST /tasks/:id/approve/:userId', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`); @@ -226,13 +194,7 @@ describe('POST /tasks/:id/approve/:userId', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); const member2Tasks = await member2.get('/tasks/user'); @@ -258,13 +220,7 @@ describe('POST /tasks/:id/approve/:userId', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); const groupTasks = await user.get(`/tasks/group/${guild._id}`); @@ -287,21 +243,10 @@ describe('POST /tasks/:id/approve/:userId', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); const member2Tasks = await member2.get('/tasks/user'); const member2SyncedTask = find(member2Tasks, findAssignedTask); - await expect(member2.post(`/tasks/${member2SyncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member2.post(`/tasks/${member2SyncedTask._id}/score/up`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`); diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js index 91026baeae..8472bc03aa 100644 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js @@ -61,13 +61,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => { let syncedTask = find(memberTasks, findAssignedTask); // score task to require approval - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.post(`/tasks/${task._id}/needs-work/${member._id}`); [memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]); @@ -114,12 +108,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => { let syncedTask = find(memberTasks, findAssignedTask); // score task to require approval - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member.post(`/tasks/${syncedTask._id}/score/up`); const initialNotifications = member.notifications.length; @@ -172,13 +161,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await member2.post(`/tasks/${task._id}/approve/${member._id}`); await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`)) .to.eventually.be.rejected.and.to.eql({ diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js index 80710c701b..810ad517d1 100644 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js @@ -44,12 +44,11 @@ describe('POST /tasks/:id/score/:direction', () => { const syncedTask = find(memberTasks, findAssignedTask); const direction = 'up'; - await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + const response = await member.post(`/tasks/${syncedTask._id}/score/${direction}`); + + expect(response.data.approvalRequested).to.equal(true); + expect(response.message).to.equal(t('taskApprovalHasBeenRequested')); + const updatedTask = await member.get(`/tasks/${syncedTask._id}`); await user.sync(); @@ -76,12 +75,7 @@ describe('POST /tasks/:id/score/:direction', () => { const syncedTask = find(memberTasks, findAssignedTask); const direction = 'up'; - await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member.post(`/tasks/${syncedTask._id}/score/${direction}`); const updatedTask = await member.get(`/tasks/${syncedTask._id}`); await user.sync(); await member2.sync(); @@ -111,12 +105,7 @@ describe('POST /tasks/:id/score/:direction', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member.post(`/tasks/${syncedTask._id}/score/up`); await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) .to.eventually.be.rejected.and.eql({ @@ -130,12 +119,7 @@ describe('POST /tasks/:id/score/:direction', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.post(`/tasks/${task._id}/approve/${member._id}`); diff --git a/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js b/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js index 45443d4f06..74c76475a2 100644 --- a/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js +++ b/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js @@ -71,12 +71,10 @@ describe('PUT /tasks/:id', () => { const syncedTask = find(memberTasks, memberTask => memberTask.group.taskId === habit._id); // score up to trigger approval - await expect(member2.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + const response = await member2.post(`/tasks/${syncedTask._id}/score/up`); + + expect(response.data.approvalRequested).to.equal(true); + expect(response.message).to.equal(t('taskApprovalHasBeenRequested')); }); it('member updates a group task value - not allowed', async () => { diff --git a/website/client/config/storybook/config.js b/website/client/config/storybook/config.js index 01bba95033..fb3957a25e 100644 --- a/website/client/config/storybook/config.js +++ b/website/client/config/storybook/config.js @@ -1,6 +1,8 @@ /* eslint-disable import/no-extraneous-dependencies */ import { configure } from '@storybook/vue'; +import './margin.css'; import '../../src/assets/scss/index.scss'; +import '../../src/assets/scss/spacing.scss'; import '../../src/assets/css/sprites.css'; import '../../src/assets/css/sprites/spritesmith-main-0.css'; diff --git a/website/client/config/storybook/margin.css b/website/client/config/storybook/margin.css new file mode 100644 index 0000000000..eaad293c27 --- /dev/null +++ b/website/client/config/storybook/margin.css @@ -0,0 +1,13 @@ +.background { + background: teal; + display: inline-block; +} + +.content { + color: white; + background: grey; +} + +.inline-block { + display: inline-block; +} diff --git a/website/client/src/assets/scss/button.scss b/website/client/src/assets/scss/button.scss index 482e731700..a19e96acf4 100644 --- a/website/client/src/assets/scss/button.scss +++ b/website/client/src/assets/scss/button.scss @@ -52,9 +52,11 @@ border-color: $purple-400; } - &:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus { - box-shadow: none; - border-color: $purple-400; + &:not(:disabled):not(.disabled) { + &:active:focus, &.active:focus { + box-shadow: none; + border-color: $purple-400; + } } &:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active { diff --git a/website/client/src/assets/scss/dropdown.scss b/website/client/src/assets/scss/dropdown.scss index 0acbce7864..4a078edd25 100644 --- a/website/client/src/assets/scss/dropdown.scss +++ b/website/client/src/assets/scss/dropdown.scss @@ -1,20 +1,27 @@ .dropdown > .btn { - padding: 9px 15.5px; + padding: 0.25rem 0.75rem; font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: normal; line-height: 1.43; } +.dropdown-toggle:hover { + --caret-color: #{$purple-200}; +} + .dropdown.show > .dropdown-toggle:not(.btn-success) { color: $purple-200; - border-color: $purple-500 !important; + border-color: $purple-400 !important; box-shadow: none; + + &::after { + --caret-color: #{$purple-200}; + } } .dropdown-toggle::after { - margin-left: 16px; - border-top: 6px solid; + border-top-color: var(--caret-color); border-right: 5px solid transparent; border-left: 5px solid transparent; vertical-align: 0; @@ -23,14 +30,18 @@ .dropdown-menu { padding: 0px; border: none; - border-radius: 4px; - box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($white, 0.1); + border-radius: 2px; + box-shadow: 0 3px 6px 0 rgba(26, 24, 29, 0.16), 0 3px 6px 0 rgba(26, 24, 29, 0.24); + } +// shared dropdown-item styles .dropdown-item { + // header items & not selectList-items padding-left: 24px; padding-top: 8px; padding-bottom: 8px; + font-size: 14px; line-height: 1.71; color: $gray-50; @@ -42,8 +53,8 @@ } - &:active, &:hover, &.active { - background-color: rgba(#d5c8ff, 0.32); + &:active, &:hover, &:focus, &.active { + background-color: rgba($purple-600, 0.32); color: $purple-200; } @@ -86,16 +97,28 @@ .dropdown-toggle { width: 100% !important; + height: 32px; text-align: left; } .dropdown-toggle::after { position: absolute; - right: 16px; - top: 17px; + right: 12px; + top: 14px; } .dropdown-menu.show { width: 100% !important; } } + +// selectList.vue items sizing +.selectListItem .dropdown-item { + padding: 0.25rem 0.75rem; + height: 32px; + + &:active, &:hover, &:focus, &.active { + background-color: rgba($purple-600, 0.25); + color: $purple-300; + } +} diff --git a/website/client/src/assets/scss/form.scss b/website/client/src/assets/scss/form.scss index edfb99245e..f4c2dc3ba1 100644 --- a/website/client/src/assets/scss/form.scss +++ b/website/client/src/assets/scss/form.scss @@ -16,10 +16,10 @@ label small { } } -// Inputs and texteares +// Inputs and textareas input, textarea, input.form-control, textarea.form-control { - padding: 10px 16px; + padding: 10px 12px; border-radius: 2px; font-size: 14px; line-height: 1.43; @@ -31,14 +31,14 @@ input, textarea, input.form-control, textarea.form-control { } &:active:not(:disabled), &:focus:not(:disabled) { - border-color: $purple-500; + border-color: $purple-400; outline: 0; box-shadow: none; } &:disabled { opacity: 0.64; - background: $gray-500; + background: $gray-700; } &.input-search { @@ -68,11 +68,48 @@ input, textarea, input.form-control, textarea.form-control { } } -.input-group { - .input-group-prepend , .input-group-append { +.input-group-outer { + display: flex; + flex-direction: row; + + .input-group { + flex: 1; + } +} + +/** Colored Input-Groups, ignoring checklist */ +.input-group:not(.checklist-group) { + border-radius: 2px; + border: solid 1px $gray-400; + + &:hover { + border-color: $gray-300; + } + + &:focus, &:active, &:focus-within { + border: solid 1px $purple-400; + } + + .input-group-prepend , .input-group-append { background: $gray-600; - color: $gray-300; - border-radius: 2px; + } +} + +/** Generic Input Group Styles */ +.input-group { + height: 2rem; + + .input-group-prepend , .input-group-append { + color: $gray-200; + border: 0; + height: 30px; + width: 2rem; + margin: 0; + + &.grow { + width: initial; + min-width: 2rem; + } &.input-group-text { font-size: 14px; @@ -83,28 +120,30 @@ input, textarea, input.form-control, textarea.form-control { } &.input-group-icon { - border: solid 1px $gray-400; - border-right: none; - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; + display: flex; + align-self: center; + align-items: center; + justify-items: center; + justify-content: center; + } + + .svg-icon { + margin: 0 !important; } &.streak-addon .svg-icon { width: 11.6px; height: 7.1px; - margin: 15px 13.4px 15.9px 13px; } &.positive-addon .svg-icon { width: 10px; height: 10px; - margin: 14px 14px; } &.negative-addon .svg-icon { width: 10px; height: 2px; - margin: 18px 14px; } } @@ -115,6 +154,19 @@ input, textarea, input.form-control, textarea.form-control { input:first-child { border-right: none !important; } + + input { + height: 30px; + border: 0; + background: $white !important; + } +} + +.input-group-spaced { + margin-left: 12px; + height: 2rem; + border-radius: 2px; + background-color: $gray-600; } .form-check { @@ -200,9 +252,13 @@ $bg-disabled-control: #34303a; align-items: center; } - .destroy-icon { - width: 14px; - height: 16px; + .destroy-icon.svg-icon { + margin-top: 1px !important; + + svg { + width: 14px; + height: 16px; + } } } diff --git a/website/client/src/assets/scss/icon.scss b/website/client/src/assets/scss/icon.scss index 5e5a9162b9..3d570b9ac0 100644 --- a/website/client/src/assets/scss/icon.scss +++ b/website/client/src/assets/scss/icon.scss @@ -1,14 +1,14 @@ .svg-icon { display: block; - transition: none !important; fill: currentColor; svg { display: block; - } + transition: none; - * { - transition: none !important; + path { + transition: none; + } } &.color { @@ -64,4 +64,4 @@ &:hover svg path { stroke: $gray-100; } -} \ No newline at end of file +} diff --git a/website/client/src/assets/scss/index.scss b/website/client/src/assets/scss/index.scss index 6842a80947..63dfcbc85f 100644 --- a/website/client/src/assets/scss/index.scss +++ b/website/client/src/assets/scss/index.scss @@ -37,3 +37,4 @@ @import './tiers'; @import './payments'; @import './datepicker.scss'; +@import './spacing'; diff --git a/website/client/src/assets/scss/misc.scss b/website/client/src/assets/scss/misc.scss index 164a05f0e7..410ddcd13a 100644 --- a/website/client/src/assets/scss/misc.scss +++ b/website/client/src/assets/scss/misc.scss @@ -14,4 +14,16 @@ border-right: 4px solid transparent; border-left: 4px solid transparent; border-bottom: 0; -} \ No newline at end of file +} + +* { + transition: none; +} + +.transition { + transition-duration: 0.15s; + transition-property: border-color, color; + transition-property: border-color, box-shadow, color; + transition-property: border-color, box-shadow, color; + transition-timing-function: ease-in; +} diff --git a/website/client/src/assets/scss/modal.scss b/website/client/src/assets/scss/modal.scss index efaf47bb0c..ae4ce79d9d 100644 --- a/website/client/src/assets/scss/modal.scss +++ b/website/client/src/assets/scss/modal.scss @@ -13,6 +13,7 @@ .modal { z-index: 1350; + padding-left: 0px !important; } .modal-dialog { diff --git a/website/client/src/assets/scss/spacing.scss b/website/client/src/assets/scss/spacing.scss new file mode 100644 index 0000000000..325c36003b --- /dev/null +++ b/website/client/src/assets/scss/spacing.scss @@ -0,0 +1,59 @@ +.m-75 { + margin: 0.75rem; // 12px +} + +.mx-75 { + margin-left: 0.75rem; + margin-right: 0.75rem; +} + +.my-75 { + margin-bottom: 0.75rem; + margin-top: 0.75rem; +} + +.mb-75 { + margin-bottom: 0.75rem; +} + +.ml-75 { + margin-left: 0.75rem; +} + +.mr-75 { + margin-right: 0.75rem; +} + +.mt-75 { + margin-top: 0.75rem; +} + +.p-75 { + padding: 0.75rem; +} + +.px-75 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.py-75 { + padding-bottom: 0.75rem; + padding-top: 0.75rem; +} + +.pb-75 { + padding-bottom: 0.75rem; +} + +.pl-75 { + padding-left: 0.75rem; +} + +.pr-75 { + padding-right: 0.75rem; +} + +.pt-75 { + padding-top: 0.75rem; +} diff --git a/website/client/src/assets/scss/task.scss b/website/client/src/assets/scss/task.scss index d651a574e7..66f6f35bf7 100644 --- a/website/client/src/assets/scss/task.scss +++ b/website/client/src/assets/scss/task.scss @@ -3,7 +3,8 @@ .habit-option-button { border: 2px solid $disabled-color; } - &:hover { + // TODO refactor to use more css-vars and less duplicate generated css code + &:hover, &:focus, &:active { .habit-option-button { border: 2px solid $active-color; } @@ -28,7 +29,7 @@ &-control { &-bg { background: $maroon-100 !important; - .habit-control:hover { background: rgba($black, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $maroon-100 !important; } @@ -40,6 +41,7 @@ &-modal { &-bg { background: $maroon-100 !important; } + &-headings { color: $white; } &-icon { color: $maroon-100 !important; } &-text { color: $red-1 !important; } &-content { @@ -58,7 +60,7 @@ &-control { &-bg { background: $red-100 !important; - .habit-control:hover { background: rgba($black, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $red-100 !important; } @@ -70,8 +72,8 @@ &-modal { &-bg { background: $red-100 !important; } + &-headings, &-text { color: $red-1 !important; } &-icon { color: $red-100 !important; } - &-text { color: $red-1 !important; } &-content { --svg-color: #{$red-100}; } @@ -89,7 +91,7 @@ &-control { &-bg { background: $orange-100 !important; - .habit-control:hover { background: rgba($orange-1, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($orange-1, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $orange-100 !important; } @@ -101,8 +103,8 @@ &-modal { &-bg { background: $orange-100 !important; } + &-headings, &-text { color: $orange-1 !important; } &-icon { color: $orange-100 !important; } - &-text { color: $orange-1 !important; } &-content { --svg-color: #{$orange-100}; } @@ -120,7 +122,7 @@ &-control { &-bg { background: $yellow-100 !important; - .habit-control:hover { background: rgba($yellow-1, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($yellow-1, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $yellow-100 !important; } @@ -132,8 +134,8 @@ &-modal { &-bg { background: $yellow-100 !important; } + &-headings, &-text { color: $yellow-1 !important; } &-icon { color: $yellow-100 !important; } - &-text { color: $yellow-1 !important; } @include modal-text-input($yellow-1); &-option-disabled:hover { .svg-icon { color: $yellow-100 !important; } @@ -151,7 +153,7 @@ &-control { &-bg { background: $green-100 !important; - .habit-control:hover { background: rgba($black, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $green-100 !important; } @@ -163,8 +165,8 @@ &-modal { &-bg { background: $green-100 !important; } + &-headings, &-text { color: $green-1 !important; } &-icon { color: $green-10 !important; } - &-text { color: $green-1 !important; } &-content { --svg-color: #{$green-100}; } @@ -183,7 +185,7 @@ &-control { &-bg { background: $teal-100 !important; - .habit-control:hover { background: rgba($black, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $teal-100 !important; } @@ -195,8 +197,8 @@ &-modal { &-bg { background: $teal-100 !important; } + &-headings, &-text { color: $teal-1 !important; } &-icon { color: $teal-100 !important; } - &-text { color: $teal-1 !important; } &-content { --svg-color: #{$teal-100}; } @@ -214,7 +216,7 @@ &-control { &-bg { background: $blue-100 !important; - .habit-control:hover { background: rgba($black, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $blue-100 !important; } @@ -226,8 +228,8 @@ &-modal { &-bg { background: $blue-100 !important; } + &-headings, &-text { color: $blue-1 !important; } &-icon { color: $blue-100 !important; } - &-text { color: $blue-1 !important; } &-content { --svg-color: #{$blue-100}; } @@ -246,7 +248,7 @@ &-control { &-bg { background: $purple-task !important; - .habit-control:hover { background: rgba($black, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-inner-habit { background: rgba($black, 0.25) !important; } @@ -256,6 +258,7 @@ &-modal { &-bg { background: $purple-300 !important; } + &-headings { color: $white; } &-icon { color: $purple-300 !important; } &-text { color: $black !important; } &-content { diff --git a/website/client/src/assets/scss/typography.scss b/website/client/src/assets/scss/typography.scss index a9caa6b285..267327b09b 100644 --- a/website/client/src/assets/scss/typography.scss +++ b/website/client/src/assets/scss/typography.scss @@ -53,7 +53,7 @@ h1 { h2 { font-size: 20px; - line-height: 1.2; + line-height: 1.4; margin-bottom: 16px; } @@ -73,3 +73,7 @@ h4 { font-family: 'Roboto Condensed', sans-serif; font-weight: bold; } + +.opacity-75 { + opacity: 0.75; +} diff --git a/website/client/src/assets/svg/information.svg b/website/client/src/assets/svg/information.svg index cb4a531a1e..c3d91e08ee 100644 --- a/website/client/src/assets/svg/information.svg +++ b/website/client/src/assets/svg/information.svg @@ -1,3 +1,3 @@ - + diff --git a/website/client/src/components/achievements/newStuff.vue b/website/client/src/components/achievements/newStuff.vue index e59592ee2a..53c427a383 100644 --- a/website/client/src/components/achievements/newStuff.vue +++ b/website/client/src/components/achievements/newStuff.vue @@ -13,20 +13,20 @@ v-html="html" > -