From e47eaf720f0c61df146d0b4559cace9aa464c98e Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Tue, 26 May 2020 19:22:21 +0300 Subject: [PATCH 01/21] Reimplement the pull command of the crowdin script --- .editorconfig | 2 +- packages/app/i18n/fr.json | 124 +++++++++++++++---------------- packages/app/i18n/index.js | 8 +- packages/app/i18n/pl.json | 54 +++++++------- packages/app/i18n/sr.json | 44 +++++------ packages/app/i18n/zh.json | 2 +- packages/scripts/i18n-crowdin.ts | 91 +++++++++++++++++++++-- packages/scripts/package.json | 5 +- yarn.lock | 38 ++++------ 9 files changed, 220 insertions(+), 148 deletions(-) diff --git a/.editorconfig b/.editorconfig index d8da069..c27b003 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,4 +9,4 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = space -indent_size = 2 +indent_size = 4 diff --git a/packages/app/i18n/fr.json b/packages/app/i18n/fr.json index 08b0799..6574a65 100644 --- a/packages/app/i18n/fr.json +++ b/packages/app/i18n/fr.json @@ -79,7 +79,7 @@ "components.contact.cannotAccessMyAccount": "Je ne peux pas accéder a mon compte", "components.contact.close": "Fermer", "components.contact.disclaimer": "Veuillez formuler vos commentaires en fournissant le plus d'informations utiles que possible pour nous aider à comprendre votre problème et à le résoudre", - "components.contact.email": "E‑mail", + "components.contact.email": "E-mail", "components.contact.foundBugOnSite": "J'ai trouvé un problème sur le site", "components.contact.improvementsSuggestion": "J'ai une suggestion pour améliorer les fonctionnalités du site", "components.contact.integrationQuestion": "Les questions d'intégration de service", @@ -91,56 +91,56 @@ "components.contact.title": "Formulaire de commentaires", "components.contact.whichQuestion": "Qu'est-ce qui vous intéresse?", "components.contact.youMessageReceived": "Votre message a été reçu. Nous vous répondrons sous peu. La réponse arrivera directement à votre adresse E‑mail:", - "components.dev.apps.accountsAllowsYouYoUseOauth2": "Ely.by Accounts service provides users with a quick and easy-to-use way to login to your site, launcher or Minecraft server via OAuth2 authorization protocol. You can find more information about integration with Ely.by Accounts in {ourDocumentation}.", - "components.dev.apps.accountsForDevelopers": "Ely.by Accounts for developers", - "components.dev.apps.addNew": "Add new", - "components.dev.apps.allRefreshTokensWillBecomeInvalid": "All \"refresh\" tokens will become invalid and after next authorization the user will get permissions prompt.", - "components.dev.apps.appAndAllTokenWillBeDeleted": "Application and all associated tokens will be deleted.", - "components.dev.apps.applicationForm.appDescriptionWillBeAlsoVisibleOnOauthPage": "Application's description will be displayed at the authorization page too. It isn't a required field. In authorization process the value may be overridden.", - "components.dev.apps.applicationForm.applicationName": "Application name:", - "components.dev.apps.applicationForm.createApplication": "Create application", - "components.dev.apps.applicationForm.creatingApplication": "Creating an application", - "components.dev.apps.applicationForm.description": "Description:", - "components.dev.apps.applicationForm.ipAddressIsOptionButPreferable": "IP address is optional, but is very preferable. It might become handy in case of we suddenly decide to play on your server with the entire band (=", - "components.dev.apps.applicationForm.minecraftServer": "Minecraft server", - "components.dev.apps.applicationForm.redirectUri": "Redirect URI:", - "components.dev.apps.applicationForm.redirectUriLimitsAllowableBaseAddress": "Redirection URI (redirectUri) determines a base address, that user will be allowed to be redirected to after authorization. In order to improve security it's better to use the whole path instead of just a domain name. For example: https://example.com/oauth/ely.", - "components.dev.apps.applicationForm.serverIp": "Server IP:", - "components.dev.apps.applicationForm.serverName": "Server name:", - "components.dev.apps.applicationForm.toDisplayRegistrationFormChooseType": "To display registration form for a new application choose necessary type.", - "components.dev.apps.applicationForm.updateApplication": "Update application", - "components.dev.apps.applicationForm.updatingApplication": "Updating an application", - "components.dev.apps.applicationForm.website": "Web site", - "components.dev.apps.applicationForm.websiteLink": "Website link:", - "components.dev.apps.applicationForm.websiteLinkWillBeUsedAsAdditionalId": "Site's link is optional, but it can be used as an additional identifier of the application.", - "components.dev.apps.applicationForm.youCanAlsoSpecifyServerSite": "You also can specify either server's site URL or its community in a social network.", - "components.dev.apps.authorization": "Authorization", - "components.dev.apps.cancel": "Cancel", - "components.dev.apps.continue": "Continue", - "components.dev.apps.countUsers": "{count, plural, =0 {No users} one {# user} other {# users}}", - "components.dev.apps.delete": "Delete", - "components.dev.apps.editDescription": "{icon} Edit description", - "components.dev.apps.feedback": "feedback", - "components.dev.apps.ifYouHaveAnyTroubles": "If you are experiencing difficulties, you can always use {feedback}. We'll surely help you.", - "components.dev.apps.ifYouSuspectingThatSecretHasBeenCompromised": "If you are suspecting that your Client Secret has been compromised, then you may want to reset it value. It'll cause recall of the all \"access\" and \"refresh\" tokens that have been issued. You can also recall all issued tokens without changing Client Secret.", - "components.dev.apps.ourDocumentation": "our documentation", - "components.dev.apps.performing": "Performing…", - "components.dev.apps.resetClientSecret": "Reset Client Secret", - "components.dev.apps.revokeAllTokens": "Revoke all tokens", - "components.dev.apps.shallWeStart": "Shall we start?", - "components.dev.apps.takeCareAccessTokensInvalidation": "Take care because \"access\" tokens won't be invalidated immediately.", - "components.dev.apps.weDontKnowAnythingAboutYou": "We don't know anything about you yet.", - "components.dev.apps.youDontHaveAnyApplication": "You don't have any app registered yet.", - "components.dev.apps.youMustAuthToBegin": "You have to authorize to start.", - "components.dev.apps.yourApplications": "Your applications:", + "components.dev.apps.accountsAllowsYouYoUseOauth2": "Le service Comptes Ely.by offre aux utilisateurs un moyen simple et rapide de se connecter à votre site, à votre launcher ou à votre serveur Minecraft via le protocole d'autorisation OAuth2. Vous pouvez trouver plus d’informations sur l’intégration avec Ely.by Accounts dans {ourDocumentation}.", + "components.dev.apps.accountsForDevelopers": "Comptes Ely.by pour les développeurs", + "components.dev.apps.addNew": "Ajouter un nouveau", + "components.dev.apps.allRefreshTokensWillBecomeInvalid": "Tous les jetons \"rafraîchir\" deviendront invalides et après la prochaine autorisation, l'utilisateur obtiendra une invite de permissions.", + "components.dev.apps.appAndAllTokenWillBeDeleted": "L'application et tous les jetons associés seront supprimés.", + "components.dev.apps.applicationForm.appDescriptionWillBeAlsoVisibleOnOauthPage": "La description de l'application sera également affichée sur la page d'autorisation. Ce n'est pas un champ obligatoire. Dans le processus d'autorisation, la valeur peut être remplacée.", + "components.dev.apps.applicationForm.applicationName": "Nom de l'application:", + "components.dev.apps.applicationForm.createApplication": "Créer une application", + "components.dev.apps.applicationForm.creatingApplication": "Création d’une application", + "components.dev.apps.applicationForm.description": "Description :", + "components.dev.apps.applicationForm.ipAddressIsOptionButPreferable": "L'adresse IP est facultative, mais très préférable. Cela pourrait devenir pratique si nous décidions soudainement de jouer sur votre serveur avec l’ensemble du groupe (=", + "components.dev.apps.applicationForm.minecraftServer": "Serveur Minecraft", + "components.dev.apps.applicationForm.redirectUri": "URL de redirection:", + "components.dev.apps.applicationForm.redirectUriLimitsAllowableBaseAddress": "L'URI de redirection (redirectUri) détermine une adresse de base à laquelle cet utilisateur sera autorisé à être redirigé après autorisation. Afin d'améliorer la sécurité, il est préférable d'utiliser l'intégralité du chemin plutôt qu'un simple nom de domaine. Par exemple: https://example.com/oauth/ely.", + "components.dev.apps.applicationForm.serverIp": "IP du Serveur:", + "components.dev.apps.applicationForm.serverName": "Nom du serveur:", + "components.dev.apps.applicationForm.toDisplayRegistrationFormChooseType": "Pour afficher le formulaire d'inscription pour une nouvelle application, sélectionnez le type nécessaire.", + "components.dev.apps.applicationForm.updateApplication": "Mettre à jour de l'application", + "components.dev.apps.applicationForm.updatingApplication": "Mise à jour d'une application", + "components.dev.apps.applicationForm.website": "Site Web", + "components.dev.apps.applicationForm.websiteLink": "Adresse du site internet:", + "components.dev.apps.applicationForm.websiteLinkWillBeUsedAsAdditionalId": "Le lien du site est facultatif, mais il peut être utilisé comme identifiant supplémentaire de l'application.", + "components.dev.apps.applicationForm.youCanAlsoSpecifyServerSite": "Vous pouvez également spécifier l'URL du site du serveur ou sa communauté dans un réseau social.", + "components.dev.apps.authorization": "Autorisation", + "components.dev.apps.cancel": "Annuler", + "components.dev.apps.continue": "Continuer", + "components.dev.apps.countUsers": "{count, plural, =0 {Pas d'utilisateurs} one {# utilisateur} other {# utilisateurs}}", + "components.dev.apps.delete": "Supprimer", + "components.dev.apps.editDescription": "{icon} Modifier la description", + "components.dev.apps.feedback": "commentaires", + "components.dev.apps.ifYouHaveAnyTroubles": "Si vous rencontrez des difficultés, vous pouvez toujours utiliser {feedback}. Nous allons sûrement vous aider.", + "components.dev.apps.ifYouSuspectingThatSecretHasBeenCompromised": "Si vous pensez que votre secret client a été compromis, vous souhaiterez peut-être réinitialiser sa valeur. Cela provoquera le rappel de tous les jetons \"d'accès\" et \"d'actualisation\" qui ont été émis. Vous pouvez également rappeler tous les jetons émis sans modifier le Client Secret.", + "components.dev.apps.ourDocumentation": "notre documentation", + "components.dev.apps.performing": "Exécution…", + "components.dev.apps.resetClientSecret": "Réinitialiser le Client Secret", + "components.dev.apps.revokeAllTokens": "Révoquer touts les tokens", + "components.dev.apps.shallWeStart": "Allons-nous commencer?", + "components.dev.apps.takeCareAccessTokensInvalidation": "Prenez-en soin parce que les jetons \"d'accès\" ne seront pas invalidés immédiatement.", + "components.dev.apps.weDontKnowAnythingAboutYou": "Nous ne savons rien de vous pour le moment.", + "components.dev.apps.youDontHaveAnyApplication": "Vous n'avez pas d'application enregistrée pour le moment.", + "components.dev.apps.youMustAuthToBegin": "Vous devez autoriser le démarrage.", + "components.dev.apps.yourApplications": "Vos applications:", "components.footerMenu.contactUs": "Nous contacter", - "components.footerMenu.forDevelopers": "For developers", + "components.footerMenu.forDevelopers": "Pour développeurs", "components.footerMenu.rules": "Règles", "components.footerMenu.siteLanguage": "Langue du site", "components.languageSwitcher.improveTranslates": "Améliorer la traduction Ely.by", - "components.languageSwitcher.improveTranslatesDescription": "Ely.by’s localization is a community effort. If you want to improve the translation of Ely.by, we'd love your help.", - "components.languageSwitcher.improveTranslatesParticipate": "Click here to participate.", - "components.languageSwitcher.mayBeInaccurate": "May be inaccurate", + "components.languageSwitcher.improveTranslatesDescription": "La traduction d'Ely.by est un effort de la communauté. Si vous souhaitez améliorer la traduction d'Ely.by, nous aimerions votre aide.", + "components.languageSwitcher.improveTranslatesParticipate": "Cliquer ici pour participer.", + "components.languageSwitcher.mayBeInaccurate": "Peut être inexacte", "components.languageSwitcher.siteLanguage": "Langue du site", "components.languageSwitcher.startTyping": "Commencer à écrire...", "components.languageSwitcher.translationProgress": "{progress}% traduit", @@ -175,31 +175,31 @@ "components.profile.changeUsername.changeUsernameWarning": "Faites attention: si vous jouez sur un serveur avec un surnom, après l'avoir changé, vous risquez de perdre toute votre progression.", "components.profile.changedAt": "Changé il y a {at}", "components.profile.disabled": "Désactivée", - "components.profile.email": "E‑mail:", + "components.profile.email": "E-mail:", "components.profile.enabled": "Activer", - "components.profile.languageIsUnavailableWarning": "The locale \"{locale}\" you've used earlier isn't currently translated enough. If you want to continue using the selected language, please {participateInTheTranslation} of the project.", + "components.profile.languageIsUnavailableWarning": "Les paramètres régionaux \"{locale}\" vous avez utilisé précédemment n'est pas traduit actuellement assez. Si vous souhaitez continuer à utiliser la langue sélectionnée, veuillez {participateInTheTranslation} du projet.", "components.profile.mojangPriorityWarning": "Un compte Mojang avec le même pseudo a été trouvé. Selon les {rules}, le propriétaire du compte a le droit d'exiger la restauration du contrôle sur le surnom.", "components.profile.multiFactorAuth.codePlaceholder": "Entrez le code ici", "components.profile.multiFactorAuth.disable": "Désactiver", "components.profile.multiFactorAuth.disableMfa": "Désactiver l'authentification à deux facteurs", - "components.profile.multiFactorAuth.disableMfaInstruction": "In order to disable two‑factor authentication, you need to provide a code from your mobile app and confirm your action with your current account password.", + "components.profile.multiFactorAuth.disableMfaInstruction": "Pour désactiver l'authentification à deux facteurs, vous devez fournir un code à partir de votre application mobile et confirmer votre action avec le mot de passe actuel de votre compte.", "components.profile.multiFactorAuth.enable": "Activer", - "components.profile.multiFactorAuth.enterCodeFromApp": "In order to finish two‑factor auth setup, please enter the code received in the mobile app:", - "components.profile.multiFactorAuth.enterKeyManually": "If you can't scan QR code, try entering your secret key manually:", + "components.profile.multiFactorAuth.enterCodeFromApp": "Afin de terminer la configuration de l'authentification à deux facteurs, veuillez entrer le code reçu dans l'application mobile:", + "components.profile.multiFactorAuth.enterKeyManually": "Si vous ne pouvez pas scanner le code QR, essayez d'entrer votre clé secrète manuellement:", "components.profile.multiFactorAuth.findAlternativeApps": "Trouver des apps alternatives", "components.profile.multiFactorAuth.installOnOfTheApps": "Installez une de ces apps:", - "components.profile.multiFactorAuth.mfaDescription": "Two‑factor authentication is an extra layer of security designed to ensure you that you're the only person who can access your account, even if the password gets stolen.", - "components.profile.multiFactorAuth.mfaEnabledForYourAcc": "Two‑factor authentication for your account is active now", - "components.profile.multiFactorAuth.mfaIntroduction": "First of all, you need to install one of our suggested apps on your phone. This app will generate login codes for you. Please choose your OS to get corresponding installation links.", - "components.profile.multiFactorAuth.mfaLoginFlowDesc": "Additional code will be requested next time you log in. Please note, that Minecraft authorization won't work when two‑factor auth is enabled.", + "components.profile.multiFactorAuth.mfaDescription": "L'authentification à deux facteurs est une couche de sécurité supplémentaire conçue pour garantir que vous êtes la seule personne pouvant accéder à votre compte, même en cas de vol du mot de passe.", + "components.profile.multiFactorAuth.mfaEnabledForYourAcc": "L'authentification à deux facteurs pour votre compte est maintenant activée", + "components.profile.multiFactorAuth.mfaIntroduction": "Tout d'abord, vous devez installer l'une de nos applications suggérées sur votre téléphone. Cette application va générer des codes de connexion pour vous. Veuillez choisir votre système d'exploitation pour obtenir les liens d'installation correspondants.", + "components.profile.multiFactorAuth.mfaLoginFlowDesc": "Un code supplémentaire vous sera demandé lors de votre prochaine connexion. Veuillez noter que l’autorisation Minecraft ne fonctionnera pas si l’authentification à deux facteurs est activée.", "components.profile.multiFactorAuth.mfaTitle": "Authentification à deux facteurs", "components.profile.multiFactorAuth.or": "OU", "components.profile.multiFactorAuth.ready": "Prêt", "components.profile.multiFactorAuth.scanQrCode": "Ouvrez votre scanner QR favoris et scannez ce code QR suivant:", "components.profile.multiFactorAuth.theAppIsInstalled": "L'application a été installé", - "components.profile.multiFactorAuth.whenKeyEntered": "If a temporary code appears in your two‑factor auth app, then you may proceed to the next step.", + "components.profile.multiFactorAuth.whenKeyEntered": "Si un code temporaire apparaît dans votre application d'authentification à deux facteurs, vous pouvez passer à l'étape suivante.", "components.profile.nickname": "Surnom:", - "components.profile.participateInTheTranslation": "participate in the translation", + "components.profile.participateInTheTranslation": "participer à la traduction", "components.profile.password": "Mot de passe:", "components.profile.passwordRequestForm.continue": "Continuer", "components.profile.passwordRequestForm.description": "Pour compléter cette action, entrez le mot de passe du compte", @@ -256,7 +256,7 @@ "services.errorsDict.emailNotFound": "L'E-mail specifié n'as pas été trouvé", "services.errorsDict.emailRequired": "L'email est obligatoire", "services.errorsDict.emailToLong": "L'email est trop longue", - "services.errorsDict.forgotYourPassword": "forgot your password", + "services.errorsDict.forgotYourPassword": "mot de passe oublié", "services.errorsDict.invalidPassword": "Vous avez entré le mauvais mot de passe pour ce compte.", "services.errorsDict.keyNotExists": "La clé est incorrecte ou a expiré.", "services.errorsDict.keyRequired": "S'il vous plaît, entrez une clé d'activation", @@ -269,10 +269,10 @@ "services.errorsDict.passwordTooShort": "Votre mot de passe doit comporter au moins 8 caractères", "services.errorsDict.passwordsDoesNotMatch": "Les mots de passe ne correspondent pas", "services.errorsDict.rePasswordRequired": "Veuillez retaper votre mot de passe", - "services.errorsDict.redirectUriInvalid": "Redirect URI is invalid", - "services.errorsDict.redirectUriRequired": "Redirect URI is required", + "services.errorsDict.redirectUriInvalid": "URL de redirection invalide", + "services.errorsDict.redirectUriRequired": "Un URL de redirection est obligatoire", "services.errorsDict.rulesAgreementRequired": "Vous devez accepter les règles afin de créer un compte", - "services.errorsDict.suggestResetPassword": "Have you {forgotYourPassword}?", + "services.errorsDict.suggestResetPassword": "Avez-vous {forgotYourPassword}?", "services.errorsDict.totpIncorrect": "Le code est incorrect", "services.errorsDict.totpRequired": "Veuillez entrer le code", "services.errorsDict.usernameInvalid": "Surnom invalide", diff --git a/packages/app/i18n/index.js b/packages/app/i18n/index.js index 46c740f..a8ae60b 100644 --- a/packages/app/i18n/index.js +++ b/packages/app/i18n/index.js @@ -17,14 +17,14 @@ module.exports = { code: 'fr', name: 'Français', englishName: 'French', - progress: 72.2, + progress: 100, isReleased: true, }, id: { code: 'id', name: 'Bahasa Indonesia', englishName: 'Indonesian', - progress: 98.7, + progress: 98, isReleased: true, }, lt: { @@ -59,14 +59,14 @@ module.exports = { code: 'sr', name: 'Српски', englishName: 'Serbian', - progress: 98.7, + progress: 100, isReleased: false, }, uk: { code: 'uk', name: 'Українська', englishName: 'Ukrainian', - progress: 98.7, + progress: 98, isReleased: true, }, vi: { diff --git a/packages/app/i18n/pl.json b/packages/app/i18n/pl.json index 7a622f9..b28911c 100644 --- a/packages/app/i18n/pl.json +++ b/packages/app/i18n/pl.json @@ -3,9 +3,9 @@ "components.accounts.goToEly": "Idź do profilu Ely.by", "components.accounts.logout": "Wyloguj się", "components.auth.acceptRules.accept": "Akceptuj", - "components.auth.acceptRules.declineAndLogout": "Wyjdź i wyloguj się", - "components.auth.acceptRules.description1": "Zaktualizowaliśmy nasze {link}", - "components.auth.acceptRules.description2": "W celu kontynuowania używania {name}, musisz je zaakceptować.", + "components.auth.acceptRules.declineAndLogout": "Odrzuć i wyloguj się", + "components.auth.acceptRules.description1": "Zaktualizowaliśmy nasze {link}.", + "components.auth.acceptRules.description2": "W celu kontynuowania używania naszej usługi, musisz zaakceptować {name}.", "components.auth.acceptRules.termsOfService": "warunki usługi", "components.auth.acceptRules.title": "Zgoda użytkownika", "components.auth.activation.accountActivationTitle": "Aktywacja konta", @@ -14,37 +14,37 @@ "components.auth.activation.confirmEmail": "Potwierdź E-mail", "components.auth.activation.didNotReceivedEmail": "Nie otrzymałeś E-mail'a?", "components.auth.activation.enterTheCode": "Wpisz tu kod z E-mail'a", - "components.auth.appInfo.appDescription": "Witamy w usłudze autoryzacji Ely.by, która umożliwia bezpieczne wykonywanie wszelkich czynności związanych z Twoim kontem. Jest to główny punkt wejścia do witryn internetowych i oprogramowania komputerowego, w tym uruchamiania gier.", + "components.auth.appInfo.appDescription": "Witamy w usłudze autoryzacji Ely.by, która umożliwia bezpieczne wykonywanie wszelkich czynności związanych z Twoim kontem. Jest to główny punkt wejścia do witryn internetowych i oprogramowania komputerowego, w tym launcherów.", "components.auth.appInfo.appName": "Konta Ely", "components.auth.appInfo.documentation": "dokumentację", "components.auth.appInfo.goToAuth": "Przejdź do autoryzacji", - "components.auth.appInfo.useItYourself": "Odwiedź naszą {link}, aby dowiedzieć się, jak korzystać z tej usługi w Twoich projektach", + "components.auth.appInfo.useItYourself": "Odwiedź naszą {link}, aby dowiedzieć się, jak korzystać z tej usługi w Twoich projektach.", "components.auth.chooseAccount.addAccount": "Zaloguj się na inne konto", "components.auth.chooseAccount.chooseAccountTitle": "Wybierz konto", "components.auth.chooseAccount.logoutAll": "Wyloguj się ze wszystkich kont", "components.auth.chooseAccount.pleaseChooseAccount": "Wybierz konto, którego chcesz używać", - "components.auth.chooseAccount.pleaseChooseAccountForApp": "Wybierz konto, które chcesz autoryzować {appName} ", + "components.auth.chooseAccount.pleaseChooseAccountForApp": "Wybierz konto, które chcesz użyć do autoryzacji {appName}", "components.auth.finish.authForAppFailed": "Autoryzacja {appName} nie powiodła się", - "components.auth.finish.authForAppSuccessful": "Autoryzacja dla {appName} została pomyślnie zakończona", + "components.auth.finish.authForAppSuccessful": "Autoryzacja {appName} została pomyślnie zakończona", "components.auth.finish.copy": "Kopiuj", "components.auth.finish.passCodeToApp": "Aby zakończyć proces autoryzacji, podaj następujący kod do {appName}", - "components.auth.finish.waitAppReaction": "Poczekaj, aż Twoja odpowiedź zostanie zgłoszona", + "components.auth.finish.waitAppReaction": "Poczekaj na odpowiedź aplikacji", "components.auth.forgotPassword.alreadyHaveCode": "Już mam kod", - "components.auth.forgotPassword.pleasePressButton": "Naciśnij przycisk poniżej, aby uzyskać wiadomość e-mail z kodem odzyskiwania hasła.", + "components.auth.forgotPassword.pleasePressButton": "Naciśnij przycisk poniżej, aby dostać wiadomość e-mail z kodem do odzyskania hasła.", "components.auth.forgotPassword.sendMail": "Wyślij maila", - "components.auth.forgotPassword.specifyEmail": "Podaj adres E-mailowy rejestracji lub ostatnią nazwę użytkownika konta, a my wyślemy wiadomość e-mail z instrukcjami dotyczącymi dalszego odzyskiwania hasła.", - "components.auth.forgotPassword.title": "Zapomniałeś hasła", + "components.auth.forgotPassword.specifyEmail": "Podaj adres E-mail który podałeś podczas rejestracji lub ostatnią nazwę użytkownika konta, a my wyślemy wiadomość e-mail z instrukcjami dotyczącymi dalszego odzyskiwania hasła.", + "components.auth.forgotPassword.title": "Odzyskiwanie hasła", "components.auth.login.createNewAccount": "Stwórz nowe konto", "components.auth.login.emailOrUsername": "Email lub nazwa użytkownika", - "components.auth.login.loginTitle": "Zaloguj", - "components.auth.login.next": "Następnie", - "components.auth.mfa.description": "Aby zalogować się na to konto, musisz wpisać jednorazowe hasło z aplikacji mobilnej", - "components.auth.mfa.enterTotp": "Wprowadź kod", + "components.auth.login.loginTitle": "Zaloguj się", + "components.auth.login.next": "Dalej", + "components.auth.mfa.description": "Aby zalogować się na to konto, musisz podać jednorazowe hasło z aplikacji mobilnej", + "components.auth.mfa.enterTotp": "Podaj kod", "components.auth.password.accountPassword": "Hasło do konta", - "components.auth.password.forgotPassword": "Zapomniałeś hasła", + "components.auth.password.forgotPassword": "Zapomniałem hasło", "components.auth.password.passwordTitle": "Wprowadź hasło", "components.auth.password.rememberMe": "Zapamiętaj mnie na tym urządzeniu", - "components.auth.password.signInButton": "Zaloguj", + "components.auth.password.signInButton": "Zaloguj się", "components.auth.permissions.approve": "Zezwól", "components.auth.permissions.decline": "Odrzuć", "components.auth.permissions.permissionsTitle": "Uprawnienia aplikacji", @@ -181,23 +181,23 @@ "components.profile.mojangPriorityWarning": "Znaleziono konto Mojang z tym samym nickiem. Zgodnie z {rules} właściciel konta ma prawo żądać przywrócenia kontroli nad pseudonimem.", "components.profile.multiFactorAuth.codePlaceholder": "Wprowadź tutaj kod", "components.profile.multiFactorAuth.disable": "Wyłącz", - "components.profile.multiFactorAuth.disableMfa": "Wyłącz uwierzytelnianie dwuetapowe", - "components.profile.multiFactorAuth.disableMfaInstruction": "Aby wyłączyć uwierzytelnianie dwuetapowe, musisz podać kod z aplikacji mobilnej i potwierdzić swoje działanie za pomocą bieżącego hasła do konta.", + "components.profile.multiFactorAuth.disableMfa": "Wyłącz weryfikację dwuetapową", + "components.profile.multiFactorAuth.disableMfaInstruction": "Aby wyłączyć weryfikację dwuetapową, musisz podać kod z aplikacji mobilnej i potwierdzić swoje działanie za pomocą bieżącego hasła do konta.", "components.profile.multiFactorAuth.enable": "Włącz", - "components.profile.multiFactorAuth.enterCodeFromApp": "Aby zakończyć konfigurację dwuetapowej autoryzacji, wpisz kod otrzymany w aplikacji mobilnej:", + "components.profile.multiFactorAuth.enterCodeFromApp": "Aby zakończyć konfigurację autoryzacji dwuetapowej, wpisz kod otrzymany w aplikacji mobilnej:", "components.profile.multiFactorAuth.enterKeyManually": "Jeśli nie możesz zeskanować kodu QR, spróbuj wpisać tajny klucz ręcznie:", "components.profile.multiFactorAuth.findAlternativeApps": "Znajdź alternatywne aplikacje", "components.profile.multiFactorAuth.installOnOfTheApps": "Zainstaluj jedną z tych aplikacji:", - "components.profile.multiFactorAuth.mfaDescription": "Uwierzytelnianie dwuetapowe to dodatkowa warstwa zabezpieczeń zaprojektowana, aby zapewnić, że jesteś jedyną osobą, która może uzyskać dostęp do Twojego konta, nawet jeśli hasło zostanie skradzione.", - "components.profile.multiFactorAuth.mfaEnabledForYourAcc": "Uwierzytelnianie dwuetapowe dla Twojego konta jest teraz aktywne", + "components.profile.multiFactorAuth.mfaDescription": "Weryfikacja dwuetapowa to dodatkowa warstwa zabezpieczeń zaprojektowana, aby zapewnić, że jesteś jedyną osobą, która może uzyskać dostęp do Twojego konta, nawet jeśli hasło zostanie skradzione.", + "components.profile.multiFactorAuth.mfaEnabledForYourAcc": "Weryfikacja dwuetapowa dla Twojego konta jest teraz aktywne", "components.profile.multiFactorAuth.mfaIntroduction": "Najpierw powinieneś zainstalować jedną z naszych sugerowanych aplikacji na telefon. Pomoże Ci ona wygenerować kody weryfikacji dwuetapowej. Wybierz swój system operacyjny, by otrzymać odpowiedni link instalacyjny.", "components.profile.multiFactorAuth.mfaLoginFlowDesc": "Dodatkowy kod będzie wymagany z następnym logowaniem. Miej to na uwadze, że autoryzacja Minecraft nie będzie działała, gdy uwierzytelnianie dwuetapowe będzie włączone.", - "components.profile.multiFactorAuth.mfaTitle": "Uwierzytelnianie dwuetapowe", + "components.profile.multiFactorAuth.mfaTitle": "Weryfikacja dwuetapowa", "components.profile.multiFactorAuth.or": "LUB", "components.profile.multiFactorAuth.ready": "Gotowe", "components.profile.multiFactorAuth.scanQrCode": "Otwórz swoją ulubioną aplikację do kodów QR oraz zeskanuj poniższy kod:", - "components.profile.multiFactorAuth.theAppIsInstalled": "Aplikacja została zainstalowana", - "components.profile.multiFactorAuth.whenKeyEntered": "Jeżeli kod tymczasowy pojawia się w twojej aplikacji uwierzytelniania dwuetapowego, możesz przejść do następnego kroku.", + "components.profile.multiFactorAuth.theAppIsInstalled": "Już ją zainstalowałem", + "components.profile.multiFactorAuth.whenKeyEntered": "Jeżeli kod tymczasowy pojawia się w twojej aplikacji weryfikacji dwuetapowej, możesz przejść do następnego kroku.", "components.profile.nickname": "Nick:", "components.profile.participateInTheTranslation": "pomóż tłumaczyć", "components.profile.password": "Hasło:", @@ -208,7 +208,7 @@ "components.profile.preferencesDescription": "Tu możesz zmienić kluczowe preferencje Twojego konta. Należy pamiętać, że wszystkie akcje muszą być potwierdzone przez wprowadzenie hasła.", "components.profile.projectRules": "zasady projektu", "components.profile.siteLanguage": "Język strony:", - "components.profile.twoFactorAuth": "Uwierzytelnianie dwuetapowe:", + "components.profile.twoFactorAuth": "Weryfikacja dwuetapowa:", "components.profile.uuid": "UUID:", "components.ui.bsod.alsoYouCanInteractWithBackground": "Możesz też pobawić się tłem - jest interaktywne ;)", "components.ui.bsod.criticalErrorHappened": "Wystąpił krytyczny błąd, przez który aplikacja nie może kontynuować normalnej pracy.", @@ -262,7 +262,7 @@ "services.errorsDict.keyRequired": "Proszę podać klucz aktywacyjny", "services.errorsDict.loginNotExist": "Przepraszamy, Ely nie rozpoznaje Twojego loginu.", "services.errorsDict.loginRequired": "Proszę podać adres E-mail lub nazwę użytkownika", - "services.errorsDict.mfaAlreadyEnabled": "Uwierzytelnianie dwuetapowe jest już włączone", + "services.errorsDict.mfaAlreadyEnabled": "Weryfikacja dwuetapowa jest już włączona", "services.errorsDict.newPasswordRequired": "Wprowadź nowe hasło", "services.errorsDict.newRePasswordRequired": "Proszę powtórzyć nowe hasło", "services.errorsDict.passwordRequired": "Proszę wpisać hasło", diff --git a/packages/app/i18n/sr.json b/packages/app/i18n/sr.json index 8953e21..311a9c0 100644 --- a/packages/app/i18n/sr.json +++ b/packages/app/i18n/sr.json @@ -13,8 +13,8 @@ "components.auth.activation.activationMailWasSentNoEmail": "Проверите е-пошту за поруке са даљим упутствима", "components.auth.activation.confirmEmail": "Потврди е-пошту", "components.auth.activation.didNotReceivedEmail": "Нисте примили е-поруку?", - "components.auth.activation.enterTheCode": "Овде унесите кôд из е-поруке", - "components.auth.appInfo.appDescription": "Ви сте на Ely.by услузи овлашћења која вам омогућава да безбедно извршите било коју операцију на налогу. Ово је једнострука тачка уноса за веб-сајтове и десктоп-софтвер, укључујући покретаче игрица.", + "components.auth.activation.enterTheCode": "Овде унесите код из е-поруке", + "components.auth.appInfo.appDescription": "Ви сте на Ely.by услузи овлашћења која вам омогућава да безбедно извршите било коју операцију на налогу. Ово је једнострука тачка уноса за веб-сајтове и десктоп софтвер, укључујући покретаче игрица.", "components.auth.appInfo.appName": "Ely налози", "components.auth.appInfo.documentation": "документацију", "components.auth.appInfo.goToAuth": "Иди на овлашћење", @@ -27,7 +27,7 @@ "components.auth.finish.authForAppFailed": "Овлашћење на апликацију {appName} није успело", "components.auth.finish.authForAppSuccessful": "Овлашћење на апликацију {appName} је успешно завршено", "components.auth.finish.copy": "Копирај", - "components.auth.finish.passCodeToApp": "Да бисте довршили процес овлашћења, пружите следећи кôд апликацији {appName}", + "components.auth.finish.passCodeToApp": "Да бисте довршили процес овлашћења, пружите следећи код апликацији {appName}", "components.auth.finish.waitAppReaction": "Сачекајте до одговора апликације", "components.auth.forgotPassword.alreadyHaveCode": "Већ имате кôд", "components.auth.forgotPassword.pleasePressButton": "Притисните дугме испод да бисте добили е-поруку са кодом за опоравак лозинке.", @@ -57,11 +57,11 @@ "components.auth.permissions.youAuthorizedAs": "Овластили сте се као:", "components.auth.recoverPassword.change": "Промени лозинку", "components.auth.recoverPassword.contactSupport": "Контактирајте са подршком", - "components.auth.recoverPassword.enterCodeBelow": "Унесите примљени кôд унутар поља испод:", + "components.auth.recoverPassword.enterCodeBelow": "Унесите примљени код унутар поља испод:", "components.auth.recoverPassword.enterNewPasswordBelow": "Унесите и поновите нову лозинку испод:", - "components.auth.recoverPassword.enterTheCode": "Унесите кôд за потврду", - "components.auth.recoverPassword.messageWasSent": "Кôд за опоравак је послат на е-пошту налога.", - "components.auth.recoverPassword.messageWasSentTo": "Кôд за опоравак је послат на е-пошту {email}.", + "components.auth.recoverPassword.enterTheCode": "Унесите код за потврду", + "components.auth.recoverPassword.messageWasSent": "Код за опоравак је послат на е-пошту налога.", + "components.auth.recoverPassword.messageWasSentTo": "Код за опоравак је послат на е-пошту {email}.", "components.auth.recoverPassword.newPassword": "Унесите нову лозинку", "components.auth.recoverPassword.newRePassword": "Поновите нову лозинку", "components.auth.recoverPassword.title": "Враћање лозинке", @@ -74,7 +74,7 @@ "components.auth.register.yourEmail": "Ваша е-пошта", "components.auth.register.yourNickname": "Ваш надимак", "components.auth.resendActivation.sendNewEmail": "Пошаљи нову е-поруку", - "components.auth.resendActivation.specifyYourEmail": "Унесите е-пошту са којом сте се регистровали и ми ћемо вам послати нови кôд за активацију", + "components.auth.resendActivation.specifyYourEmail": "Унесите е-пошту са којом сте се регистровали и ми ћемо вам послати нови код за активацију", "components.auth.resendActivation.title": "Нисте примили е-поруку", "components.contact.cannotAccessMyAccount": "Не могу да приступим налогу", "components.contact.close": "Затвори", @@ -113,7 +113,7 @@ "components.dev.apps.applicationForm.website": "Веб-сајт", "components.dev.apps.applicationForm.websiteLink": "Веза до веб-сајта:", "components.dev.apps.applicationForm.websiteLinkWillBeUsedAsAdditionalId": "Веза до сајта је опционална, али се може користити као додатни идентификатор апликације.", - "components.dev.apps.applicationForm.youCanAlsoSpecifyServerSite": "Такође, можете да наведете или URL сајта сервера или његову заједницу на друштвеној мрежи.", + "components.dev.apps.applicationForm.youCanAlsoSpecifyServerSite": "Можете и да наведете или URL сајта сервера или његову заједницу на друштвеној мрежи.", "components.dev.apps.authorization": "Овлашћење", "components.dev.apps.cancel": "Откажи", "components.dev.apps.continue": "Настави", @@ -154,10 +154,10 @@ "components.profile.changeEmail.changeEmailTitle": "Промена е-поште", "components.profile.changeEmail.codePlaceholder": "Овде налепите кôд", "components.profile.changeEmail.currentAccountEmail": "Тренутна е-адреса налога:", - "components.profile.changeEmail.enterFinalizationCode": "Како бисте потврдили нову е-пошту, унесите примљени кôд унутар поља испод:", - "components.profile.changeEmail.enterInitializationCode": "Е-порука са кодом за покретање процедуре промене е-поште је послата на е-пошту {email}. Унесите кôд унутар поља испод:", + "components.profile.changeEmail.enterFinalizationCode": "Како бисте потврдили нову е-пошту, унесите примљени код унутар поља испод:", + "components.profile.changeEmail.enterInitializationCode": "Е-порука са кодом за покретање процедуре промене е-поште је послата на е-пошту {email}. Унесите код унутар поља испод:", "components.profile.changeEmail.enterNewEmail": "Затим пружите нову адресу е-поште коју желите да користите са овим налогом. Послаћемо вам е-поруку са кодом за потврду.", - "components.profile.changeEmail.finalizationCodeWasSentToEmail": "Кôд за потврду промене е-поште је послат на е-пошту {email}.", + "components.profile.changeEmail.finalizationCodeWasSentToEmail": "Код за потврду промене е-поште је послат на е-пошту {email}.", "components.profile.changeEmail.newEmailPlaceholder": "Унесите нову е-пошту", "components.profile.changeEmail.pressButtonToStart": "Притисните дугме испод да бисте послали поруку са кодом за покретање промене е-поште.", "components.profile.changeEmail.sendEmailButton": "Пошаљи е-поруку", @@ -177,29 +177,29 @@ "components.profile.disabled": "онемогућена", "components.profile.email": "Е-пошта:", "components.profile.enabled": "омогућена", - "components.profile.languageIsUnavailableWarning": "The locale \"{locale}\" you've used earlier isn't currently translated enough. If you want to continue using the selected language, please {participateInTheTranslation} of the project.", + "components.profile.languageIsUnavailableWarning": "Локални стандард „{locale}” који сте користили раније тренутно није довољно преведен. Ако желите да наставите са коришћењем изабраног језика, {participateInTheTranslation} пројекта.", "components.profile.mojangPriorityWarning": "Пронађен је Mojang налог са истим надимком. Према {rules}, власник налога има право да затражи враћање контроле над надимком.", "components.profile.multiFactorAuth.codePlaceholder": "Овде унесите кôд", "components.profile.multiFactorAuth.disable": "Онемогући", "components.profile.multiFactorAuth.disableMfa": "Онемогућите потврду идентитета помоћу два фактора", - "components.profile.multiFactorAuth.disableMfaInstruction": "Како бисте онемогућили потврду идентитета помоћу два фактора, морате да пружите кôд из мобилне апликације и потврдите радњу са тренутном лозинком налога.", + "components.profile.multiFactorAuth.disableMfaInstruction": "Како бисте онемогућили потврду идентитета помоћу два фактора, морате да пружите код из мобилне апликације и потврдите радњу са тренутном лозинком налога.", "components.profile.multiFactorAuth.enable": "Омогући", - "components.profile.multiFactorAuth.enterCodeFromApp": "Како бисте довршили подешавање потврде идентитета у два фактора, унесите примљени кôд у мобилну апликацију:", - "components.profile.multiFactorAuth.enterKeyManually": "Ако не можете да скенирате QR кôд, покушајте ручно да унесете тајни кључ:", + "components.profile.multiFactorAuth.enterCodeFromApp": "Како бисте довршили подешавање потврде идентитета у два фактора, унесите примљени код у мобилну апликацију:", + "components.profile.multiFactorAuth.enterKeyManually": "Ако не можете да скенирате QR код, покушајте ручно да унесете тајни кључ:", "components.profile.multiFactorAuth.findAlternativeApps": "Пронађи алтернативне апликације", "components.profile.multiFactorAuth.installOnOfTheApps": "Инсталирајте једну од следећих апликација:", "components.profile.multiFactorAuth.mfaDescription": "Потврда идентитета помоћу два фактора је додатни слој безбедности осмишљен да осигура да сте једина особа која може приступити налогу, чак и ако се лозинка украде.", "components.profile.multiFactorAuth.mfaEnabledForYourAcc": "Потврда идентитета помоћу два фактора за налог је сада активна", "components.profile.multiFactorAuth.mfaIntroduction": "Пре свега, морате да инсталирате једну од наших предложених апликација на телефону. Ова апликација ће генерисати кодове за вас. Одаберите оперативни систем да бисте добили одговарајуће везе за инсталацију.", - "components.profile.multiFactorAuth.mfaLoginFlowDesc": "Додатни кôд ће бити затражен следећи пут када се пријавите. Запамтите да овлашћење на Minecraft неће радити када је омогућена потврда идентитета помоћу два фактора.", + "components.profile.multiFactorAuth.mfaLoginFlowDesc": "Додатни код биће затражен следећи пут када се пријавите. Запамтите да овлашћење на Minecraft неће радити када је омогућена потврда идентитета помоћу два фактора.", "components.profile.multiFactorAuth.mfaTitle": "Потврда идентитета помоћу два фактора", "components.profile.multiFactorAuth.or": "ИЛИ", "components.profile.multiFactorAuth.ready": "Спремно", - "components.profile.multiFactorAuth.scanQrCode": "Отворите омиљену апликацију-скенер QR-а и скенирајте следећи QR кôд:", + "components.profile.multiFactorAuth.scanQrCode": "Отворите омиљену апликацију скенера QR-а и скенирајте следећи код:", "components.profile.multiFactorAuth.theAppIsInstalled": "Апликација је инсталирана", - "components.profile.multiFactorAuth.whenKeyEntered": "Ако се привремени кôд појави у апликацији за потврду идентитета помоћу два фактора, можете прећи на следећи корак.", + "components.profile.multiFactorAuth.whenKeyEntered": "Ако се привремени код појави у апликацији за потврду идентитета помоћу два фактора, можете прећи на следећи корак.", "components.profile.nickname": "Надимак:", - "components.profile.participateInTheTranslation": "participate in the translation", + "components.profile.participateInTheTranslation": "учествујте у превођењу", "components.profile.password": "Лозинка:", "components.profile.passwordRequestForm.continue": "Настави", "components.profile.passwordRequestForm.description": "Да бисте довршили радњу, унесите лозинку налога", @@ -210,7 +210,7 @@ "components.profile.siteLanguage": "Језик сајта:", "components.profile.twoFactorAuth": "Потврда идентитета помоћу два фактора", "components.profile.uuid": "UUID:", - "components.ui.bsod.alsoYouCanInteractWithBackground": "Такође се можете играти позадином — интерактивна је ;)", + "components.ui.bsod.alsoYouCanInteractWithBackground": "Можете и да се играте позадином — интерактивна је ;)", "components.ui.bsod.criticalErrorHappened": "Дошло је до критичне грешке због које апликација не може нормално да настави са радом.", "components.ui.bsod.reloadPageOrContactUs": "Поново учитајте ову страницу, па покушајте поново. Ако се проблем и даље буде јављао, пријавите га програмерима слањем е-поруке на", "components.userbar.login": "Пријава", @@ -273,7 +273,7 @@ "services.errorsDict.redirectUriRequired": "URI преусмеравања је обавезан", "services.errorsDict.rulesAgreementRequired": "Морате да прихватите правила како бисте отворили налог", "services.errorsDict.suggestResetPassword": "Нисте ли {forgotYourPassword}?", - "services.errorsDict.totpIncorrect": "Кôд је неисправан", + "services.errorsDict.totpIncorrect": "Код није исправан", "services.errorsDict.totpRequired": "Унесите кôд", "services.errorsDict.usernameInvalid": "Корисничко име је неважеће", "services.errorsDict.usernameRequired": "Корисничко име је обавезно", diff --git a/packages/app/i18n/zh.json b/packages/app/i18n/zh.json index 5adff9a..fd1d1d8 100644 --- a/packages/app/i18n/zh.json +++ b/packages/app/i18n/zh.json @@ -49,7 +49,7 @@ "components.auth.permissions.decline": "拒绝", "components.auth.permissions.permissionsTitle": "应用程序权限", "components.auth.permissions.scope_account_email": "访问您的邮件地址", - "components.auth.permissions.scope_account_info": "访问您的个人资料数据(除了邮件)", + "components.auth.permissions.scope_account_info": "访问您的个人资料数据(除了电子邮件地址)", "components.auth.permissions.scope_minecraft_server_session": "Minecraft服务器的授权数据", "components.auth.permissions.scope_offline_access": "当您离线时访问您的个人资料数据", "components.auth.permissions.theAppNeedsAccess1": "此应用程序需要访问", diff --git a/packages/scripts/i18n-crowdin.ts b/packages/scripts/i18n-crowdin.ts index 69282b6..8341449 100644 --- a/packages/scripts/i18n-crowdin.ts +++ b/packages/scripts/i18n-crowdin.ts @@ -3,7 +3,10 @@ import fs from 'fs'; import path from 'path'; +import axios from 'axios'; +import JSON5 from 'json5'; import CrowdinApi, { LanguageStatusNode, LanguageStatusResponse, ProjectInfoResponse } from 'crowdin-api'; +import { TranslationStatus, Translations, Credentials } from '@crowdin/crowdin-api-client'; import MultiProgress from 'multi-progress'; import ch from 'chalk'; import iso639 from 'iso-639-1'; @@ -18,7 +21,9 @@ if (!config.crowdinApiKey) { process.exit(126); } -const PROJECT_ID = 'elyby'; +const ORGANIZATION_ID = 'elyby'; +const PROJECT_ID = 350687; +const FILE_ID = 6; const PROJECT_KEY = config.crowdinApiKey; const CROWDIN_FILE_PATH = 'accounts/site.json'; const SOURCE_LANG = 'en'; @@ -26,16 +31,22 @@ const LANG_DIR = path.resolve(`${__dirname}/../app/i18n`); const INDEX_FILE_NAME = 'index.js'; const MIN_RELEASE_PROGRESS = 80; // Minimal ready percent before translation can be published +const credentials: Credentials = { + token: config.crowdinApiKey, +}; +const translationStatusApi = new TranslationStatus(credentials); +const translationsApi = new Translations(credentials); + const crowdin = new CrowdinApi({ apiKey: PROJECT_KEY, - projectName: PROJECT_ID, + projectName: ORGANIZATION_ID, }); const progressBar = new MultiProgress(); /** * Locales that has been verified by core team members */ -const RELEASED_LOCALES: Array = ['be', 'fr', 'id', 'pt', 'ru', 'uk', 'vi', 'zh']; +const releasedLocales: Array = ['be', 'fr', 'id', 'pt', 'ru', 'uk', 'vi', 'zh']; /** * Array of Crowdin locales to our internal locales representation @@ -80,7 +91,7 @@ function toInternalLocale(code: string): string { * хранятся в самом приложении */ function serializeToModule(translates: Record): string { - const src = JSON.stringify(sortByKeys(translates), null, 2); + const src = JSON5.stringify(sortByKeys(translates), null, 4); return `module.exports = ${src};\n`; } @@ -129,6 +140,70 @@ interface IndexFileEntry { isReleased: boolean; } +async function pullNew(): Promise { + console.log('Pulling translation progress...'); + const { data: translationProgress } = await translationStatusApi.getFileProgress(PROJECT_ID, FILE_ID, 100); + + const localesToPull: Array = []; + const indexFileEntries: Record = { + en: { + code: 'en', + name: 'English', + englishName: 'English', + progress: 100, + isReleased: true, + }, + }; + + translationProgress.forEach(({ data: { languageId, approvalProgress } }) => { + const locale = toInternalLocale(languageId); + if (releasedLocales.includes(locale) || approvalProgress >= MIN_RELEASE_PROGRESS) { + localesToPull.push(languageId); + indexFileEntries[locale] = { + code: locale, + name: NATIVE_NAMES_MAP[locale] || iso639.getNativeName(locale), + englishName: ENGLISH_NAMES_MAP[locale] || iso639.getName(locale), + progress: approvalProgress, + isReleased: releasedLocales.includes(locale), + }; + } + }); + + // Add prefix 'c' to current and total to prevent filling thees placeholders with real values + const downloadingProgressBar = progressBar.newBar('Downloading translates :bar :percent | :cCurrent/:total', { + total: localesToPull.length, + incomplete: '\u2591', + complete: '\u2588', + width: Object.keys(indexFileEntries).length - 1, + }); + let downloadingReady = 0; + + const promises = localesToPull.map(async (languageId): Promise => { + const { data: { url } } = await translationsApi.buildProjectFileTranslation(PROJECT_ID, FILE_ID, { + targetLanguageId: languageId, + exportApprovedOnly: true, + }); + + const fileResponse = await axios.get(url, { + // Disable response parsing + transformResponse: [], + }); + fs.writeFileSync(path.join(LANG_DIR, `${toInternalLocale(languageId)}.json`), fileResponse.data); + + downloadingProgressBar.update(++downloadingReady / localesToPull, { + cCurrent: downloadingReady, + }); + }); + + await Promise.all(promises); + + console.log('Writing an index file'); + + fs.writeFileSync(path.join(LANG_DIR, INDEX_FILE_NAME), serializeToModule(indexFileEntries)); + + console.log(ch.green('The index file was successfully written')); +} + async function pull() { console.log('Pulling locales list...'); const locales = await pullLocales(); @@ -169,7 +244,7 @@ async function pull() { const progress = (fileInfo.words_approved / fileInfo.words) * 100; - if (!RELEASED_LOCALES.includes(toInternalLocale(locale.code)) && progress < MIN_RELEASE_PROGRESS) { + if (!releasedLocales.includes(toInternalLocale(locale.code)) && progress < MIN_RELEASE_PROGRESS) { return null; } @@ -196,7 +271,7 @@ async function pull() { console.log('Locales are downloaded. Writing them to file system.'); - const indexFileEntries: { [key: string]: IndexFileEntry } = { + const indexFileEntries: Record = { en: { code: 'en', name: 'English', @@ -223,7 +298,7 @@ async function pull() { name: NATIVE_NAMES_MAP[ourCode] || iso639.getNativeName(ourCode), englishName: ENGLISH_NAMES_MAP[ourCode] || name, progress: parseFloat(progress.toFixed(1)), - isReleased: RELEASED_LOCALES.includes(ourCode), + isReleased: releasedLocales.includes(ourCode), }; fs.copyFile(translatesFilePath, path.join(LANG_DIR, `${ourCode}.json`), 0, (err) => { @@ -286,7 +361,7 @@ try { switch (action) { case 'pull': - pull(); + pullNew(); break; case 'push': push(); diff --git a/packages/scripts/package.json b/packages/scripts/package.json index d817950..14c3251 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -11,15 +11,18 @@ "license": "ISC", "dependencies": { "@babel/node": "^7.8.3", + "@crowdin/crowdin-api-client": "^1.8.0", "@types/mkdirp": "^1.0.0", "@types/progress": "^2.0.3", + "axios": "^0.19.2", "chalk": "^4.0.0", "crowdin-api": "^4.0.0", "glob": "^7.1.6", "iso-639-1": "^2.1.3", + "json5": "^2.1.3", "mkdirp": "^1.0.4", "multi-progress": "^2.0.0", - "prompt": "^1.0.0", + "prompt": "https://github.com/flatiron/prompt.git#master", "utility-types": "^3.10.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 36f6791..dcbfd8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1905,6 +1905,13 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@crowdin/crowdin-api-client@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@crowdin/crowdin-api-client/-/crowdin-api-client-1.8.0.tgz#017a7c62dd6fd115444a172620a9d5f937ec2fdf" + integrity sha512-OyQbSbIHnURNyAESuhE6ZK5WB0r0JKjMClo39Bc6+95UKNzy1rFTrF33W1stRJs7j6WfuOf3sYsPXu9deyYURQ== + dependencies: + axios "^0.19.0" + "@cypress/listr-verbose-renderer@0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" @@ -4293,7 +4300,7 @@ axios@0.19.0: follow-redirects "1.5.10" is-buffer "^2.0.2" -axios@^0.19.2: +axios@^0.19.0, axios@^0.19.2: version "0.19.2" resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== @@ -9998,7 +10005,7 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.0, json5@^2.1.2: +json5@^2.1.0, json5@^2.1.2, json5@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== @@ -12006,16 +12013,6 @@ pkg-up@2.0.0, pkg-up@^2.0.0: dependencies: find-up "^2.1.0" -pkginfo@0.3.x: - version "0.3.1" - resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" - integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE= - -pkginfo@0.x.x: - version "0.4.1" - resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff" - integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8= - please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" @@ -12608,17 +12605,15 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prompt@^1.0.0: +"prompt@https://github.com/flatiron/prompt.git#master": version "1.0.0" - resolved "https://registry.yarnpkg.com/prompt/-/prompt-1.0.0.tgz#8e57123c396ab988897fb327fd3aedc3e735e4fe" - integrity sha1-jlcSPDlquYiJf7Mn/Trtw+c15P4= + resolved "https://github.com/flatiron/prompt.git#8d5495c84c3f433b8f26ea2798f8ba68c3656459" dependencies: colors "^1.1.2" - pkginfo "0.x.x" read "1.0.x" revalidator "0.1.x" utile "0.3.x" - winston "2.1.x" + winston "2.x" prompts@^2.0.1: version "2.3.0" @@ -16208,17 +16203,16 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" -winston@2.1.x: - version "2.1.1" - resolved "https://registry.yarnpkg.com/winston/-/winston-2.1.1.tgz#3c9349d196207fd1bdff9d4bc43ef72510e3a12e" - integrity sha1-PJNJ0ZYgf9G9/51LxD73JRDjoS4= +winston@2.x: + version "2.4.4" + resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.4.tgz#a01e4d1d0a103cf4eada6fc1f886b3110d71c34b" + integrity sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q== dependencies: async "~1.0.0" colors "1.0.x" cycle "1.0.x" eyes "0.1.x" isstream "0.1.x" - pkginfo "0.3.x" stack-trace "0.0.x" word-wrap@^1.2.3, word-wrap@~1.2.3: From 5e62c930b151f9d6d491b7a9d97159d8f069206e Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Wed, 27 May 2020 13:07:25 +0300 Subject: [PATCH 02/21] Cleanup the crowdin integration script, completely migrate it to API v2 --- @types/cwordin-api.d.ts | 98 ----------- @types/multi-progress.d.ts | 10 -- @types/prompt.d.ts | 56 ------ babel.config.js | 12 +- config.js | 12 +- packages/scripts/i18n-crowdin.ts | 291 +++++++++---------------------- packages/scripts/package.json | 11 +- yarn.lock | 160 +++++------------ 8 files changed, 161 insertions(+), 489 deletions(-) delete mode 100644 @types/cwordin-api.d.ts delete mode 100644 @types/multi-progress.d.ts delete mode 100644 @types/prompt.d.ts diff --git a/@types/cwordin-api.d.ts b/@types/cwordin-api.d.ts deleted file mode 100644 index 9ca2289..0000000 --- a/@types/cwordin-api.d.ts +++ /dev/null @@ -1,98 +0,0 @@ -declare module 'crowdin-api' { - export interface ProjectInfoFile { - node_type: 'file'; - id: number; - name: string; - created: string; - last_updated: string; - last_accessed: string; - last_revision: string; - } - - export interface ProjectInfoDirectory { - node_type: 'directory'; - id: number; - name: string; - files: Array; - } - - export interface ProjectInfoResponse { - details: { - source_language: { - name: string; - code: string; - }; - name: string; - identifier: string; - created: string; - description: string; - join_policy: string; - last_build: string | null; - last_activity: string; - participants_count: string; // it's number, but string in the response - logo_url: string | null; - total_strings_count: string; // it's number, but string in the response - total_words_count: string; // it's number, but string in the response - duplicate_strings_count: number; - duplicate_words_count: number; - invite_url: { - translator: string; - proofreader: string; - }; - }; - languages: Array<{ - name: string; // English language name - code: string; - can_translate: 0 | 1; - can_approve: 0 | 1; - }>; - files: Array; - } - - export interface LanguageStatusNode { - node_type: 'directory' | 'file'; - id: number; - name: string; - phrases: number; - translated: number; - approved: number; - words: number; - words_translated: number; - words_approved: number; - files: Array; - } - - export interface LanguageStatusResponse { - files: Array; - } - - type FilesList = Record; - - export default class CrowdinApi { - constructor(params: { apiKey: string; projectName: string; baseUrl?: string }); - projectInfo(): Promise; - languageStatus(language: string): Promise; - exportFile( - file: string, - language: string, - params?: { - branch?: string; - format?: 'xliff'; - export_translated_only?: boolean; - export_approved_only?: boolean; - }, - ): Promise; // TODO: not sure about Promise return type - updateFile( - files: FilesList, - params: { - titles?: Record; - export_patterns?: Record; - new_names?: Record; - first_line_contains_header?: string; - scheme?: string; - update_option?: 'update_as_unapproved' | 'update_without_changes'; - branch?: string; - }, - ): Promise; - } -} diff --git a/@types/multi-progress.d.ts b/@types/multi-progress.d.ts deleted file mode 100644 index 67df384..0000000 --- a/@types/multi-progress.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare module 'multi-progress' { - export default class MultiProgress { - constructor(stream?: string); - newBar(schema: string, options: ProgressBar.ProgressBarOptions): ProgressBar; - terminate(): void; - move(index: number): void; - tick(index: number, value?: number, options?: any): void; - update(index: number, value?: number, options?: any): void; - } -} diff --git a/@types/prompt.d.ts b/@types/prompt.d.ts deleted file mode 100644 index 14a2e70..0000000 --- a/@types/prompt.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Type definitions for Prompt.js 1.0.0 -// Project: https://github.com/flatiron/prompt - -declare module 'prompt' { - type PropertiesType = Array | prompt.PromptSchema | Array; - - namespace prompt { - interface PromptSchema { - properties: PromptProperties; - } - - interface PromptProperties { - [propName: string]: PromptPropertyOptions; - } - - interface PromptPropertyOptions { - name?: string; - pattern?: RegExp; - message?: string; - required?: boolean; - hidden?: boolean; - description?: string; - type?: string; - default?: string; - before?: (value: any) => any; - conform?: (result: any) => boolean; - } - - export function start(): void; - - export function get( - properties: T, - callback: ( - err: Error, - result: T extends Array - ? Record - : T extends PromptSchema - ? Record - : T extends Array - ? Record - : never, - ) => void, - ): void; - - export function addProperties(obj: any, properties: PropertiesType, callback: (err: Error) => void): void; - - export function history(propertyName: string): any; - - export let override: any; - export let colors: boolean; - export let message: string; - export let delimiter: string; - } - - export = prompt; -} diff --git a/babel.config.js b/babel.config.js index 028a217..c95ccf7 100644 --- a/babel.config.js +++ b/babel.config.js @@ -8,7 +8,16 @@ module.exports = { }, ], '@babel/preset-react', - '@babel/preset-env', + [ + '@babel/preset-env', + { + ignoreBrowserslistConfig: true, + targets: { + node: true, + }, + modules: 'commonjs', + }, + ], ], plugins: [ '@babel/plugin-syntax-dynamic-import', @@ -29,6 +38,7 @@ module.exports = { [ '@babel/preset-env', { + ignoreBrowserslistConfig: false, modules: false, useBuiltIns: 'usage', // or "entry" corejs: 3, diff --git a/config.js b/config.js index fe65a79..e6b90a2 100644 --- a/config.js +++ b/config.js @@ -1,4 +1,7 @@ /* eslint-env node */ +/* eslint-disable @typescript-eslint/no-var-requires */ + +const path = require('path'); require('dotenv').config(); @@ -9,5 +12,12 @@ module.exports = { apiHost: env.API_HOST || 'https://dev.account.ely.by', ga: env.GA_ID && { id: env.GA_ID }, sentryDSN: env.SENTRY_DSN, - crowdinApiKey: env.CROWDIN_API_KEY, + crowdin: { + apiKey: env.CROWDIN_API_KEY, + projectId: 350687, + filePath: 'accounts/site.json', + sourceLang: 'en', + basePath: path.resolve(`${__dirname}/packages/app/i18n`), + minApproved: 80, // Minimal ready percent before translation can be published + }, }; diff --git a/packages/scripts/i18n-crowdin.ts b/packages/scripts/i18n-crowdin.ts index 8341449..f852567 100644 --- a/packages/scripts/i18n-crowdin.ts +++ b/packages/scripts/i18n-crowdin.ts @@ -3,53 +3,40 @@ import fs from 'fs'; import path from 'path'; + import axios from 'axios'; import JSON5 from 'json5'; -import CrowdinApi, { LanguageStatusNode, LanguageStatusResponse, ProjectInfoResponse } from 'crowdin-api'; -import { TranslationStatus, Translations, Credentials } from '@crowdin/crowdin-api-client'; -import MultiProgress from 'multi-progress'; +import Crowdin, { SourceFilesModel } from '@crowdin/crowdin-api-client'; +import ProgressBar from 'progress'; import ch from 'chalk'; import iso639 from 'iso-639-1'; -import prompt from 'prompt'; +import { prompt } from 'inquirer'; -import { ValuesType } from 'utility-types'; +import config from './../../config'; -import config from '../../config'; - -if (!config.crowdinApiKey) { +if (!config.crowdin.apiKey) { console.error(ch.red`crowdinApiKey is required`); process.exit(126); } -const ORGANIZATION_ID = 'elyby'; -const PROJECT_ID = 350687; -const FILE_ID = 6; -const PROJECT_KEY = config.crowdinApiKey; -const CROWDIN_FILE_PATH = 'accounts/site.json'; -const SOURCE_LANG = 'en'; -const LANG_DIR = path.resolve(`${__dirname}/../app/i18n`); +const PROJECT_ID = config.crowdin.projectId; +const CROWDIN_FILE_PATH = config.crowdin.filePath; +const SOURCE_LANG = config.crowdin.sourceLang; +const LANG_DIR = config.crowdin.basePath; const INDEX_FILE_NAME = 'index.js'; -const MIN_RELEASE_PROGRESS = 80; // Minimal ready percent before translation can be published +const MIN_RELEASE_PROGRESS = config.crowdin.minApproved; -const credentials: Credentials = { - token: config.crowdinApiKey, -}; -const translationStatusApi = new TranslationStatus(credentials); -const translationsApi = new Translations(credentials); - -const crowdin = new CrowdinApi({ - apiKey: PROJECT_KEY, - projectName: ORGANIZATION_ID, +const crowdin = new Crowdin({ + token: config.crowdin.apiKey, }); -const progressBar = new MultiProgress(); /** * Locales that has been verified by core team members */ -const releasedLocales: Array = ['be', 'fr', 'id', 'pt', 'ru', 'uk', 'vi', 'zh']; +const releasedLocales: ReadonlyArray = ['be', 'fr', 'id', 'pt', 'ru', 'uk', 'vi', 'zh']; /** - * Array of Crowdin locales to our internal locales representation + * Map Crowdin locales into our internal locales representation */ const LOCALES_MAP: Record = { 'pt-BR': 'pt', @@ -57,7 +44,8 @@ const LOCALES_MAP: Record = { }; /** - * This array allows us to customise native languages names, because ISO-639-1 sometimes is strange + * This array allows us to customise native languages names, + * because ISO-639-1 sometimes is strange */ const NATIVE_NAMES_MAP: Record = { be: 'Беларуская', @@ -108,30 +96,6 @@ function sortByKeys>(object: T): T { }, {} as T); } -async function pullLocales(): Promise { - const { languages } = await crowdin.projectInfo(); - - return languages; -} - -function findFile(root: LanguageStatusResponse['files'], path: string): LanguageStatusNode | null { - const [nodeToSearch, ...rest] = path.split('/'); - - for (const node of root) { - if (node.name !== nodeToSearch) { - continue; - } - - if (rest.length === 0) { - return node; - } - - return findFile(node.files, rest.join('/')); - } - - return null; -} - interface IndexFileEntry { code: string; name: string; @@ -140,9 +104,48 @@ interface IndexFileEntry { isReleased: boolean; } -async function pullNew(): Promise { +function getLocaleFilePath(languageId: string): string { + return path.join(LANG_DIR, `${toInternalLocale(languageId)}.json`); +} + +let directoriesList: Array; +let filesList: Array; + +async function findFileId(path: string, parentDir: number|null = null): Promise { + const [nodeToSearch, ...rest] = path.split('/'); + if (rest.length === 0) { + if (!filesList) { + const { data: filesResponse } = await crowdin.sourceFilesApi.listProjectFiles(PROJECT_ID); + filesList = filesResponse.map((fileData) => fileData.data); + } + + const file = filesList.find((file) => file.directoryId === parentDir && file.name === nodeToSearch); + if (file === undefined) { + throw new Error('Cannot find file by provided path'); + } + + return file.id; + } + + if (!directoriesList) { + const { data: dirsResponse } = await crowdin.sourceFilesApi.listProjectDirectories(PROJECT_ID); + directoriesList = dirsResponse.map((dirData) => dirData.data); + } + + const dir = directoriesList.find((dir) => dir.directoryId === parentDir && dir.name === nodeToSearch); + if (dir === undefined) { + throw new Error('Cannot find directory by provided path'); + } + + return findFileId(rest.join('/'), dir.id); +} + +async function pull(): Promise { + console.log('Loading file info...'); + const fileId = await findFileId(CROWDIN_FILE_PATH); + console.log('Pulling translation progress...'); - const { data: translationProgress } = await translationStatusApi.getFileProgress(PROJECT_ID, FILE_ID, 100); + const { data: translationProgress } = await crowdin.translationStatusApi.getFileProgress(PROJECT_ID, fileId, 100); const localesToPull: Array = []; const indexFileEntries: Record = { @@ -170,7 +173,7 @@ async function pullNew(): Promise { }); // Add prefix 'c' to current and total to prevent filling thees placeholders with real values - const downloadingProgressBar = progressBar.newBar('Downloading translates :bar :percent | :cCurrent/:total', { + const downloadingProgressBar = new ProgressBar('Downloading translates :bar :percent | :cCurrent/:total', { total: localesToPull.length, incomplete: '\u2591', complete: '\u2588', @@ -179,18 +182,18 @@ async function pullNew(): Promise { let downloadingReady = 0; const promises = localesToPull.map(async (languageId): Promise => { - const { data: { url } } = await translationsApi.buildProjectFileTranslation(PROJECT_ID, FILE_ID, { + const { data: { url } } = await crowdin.translationsApi.buildProjectFileTranslation(PROJECT_ID, fileId, { targetLanguageId: languageId, exportApprovedOnly: true, }); - const fileResponse = await axios.get(url, { + const { data: fileContents } = await axios.get(url, { // Disable response parsing transformResponse: [], }); - fs.writeFileSync(path.join(LANG_DIR, `${toInternalLocale(languageId)}.json`), fileResponse.data); + fs.writeFileSync(getLocaleFilePath(languageId), fileContents); - downloadingProgressBar.update(++downloadingReady / localesToPull, { + downloadingProgressBar.update(++downloadingReady / localesToPull.length, { cCurrent: downloadingReady, }); }); @@ -204,156 +207,32 @@ async function pullNew(): Promise { console.log(ch.green('The index file was successfully written')); } -async function pull() { - console.log('Pulling locales list...'); - const locales = await pullLocales(); - const checkingProgressBar = progressBar.newBar('| Pulling locales info :bar :percent | :current/:total', { - total: locales.length, - incomplete: '\u2591', - complete: '\u2588', - width: locales.length, - }); - // Add prefix 'c' to current and total to prevent filling thees placeholders with real values - const downloadingProgressBar = progressBar.newBar('| Downloading translates :bar :percent | :cCurrent/:cTotal', { - total: 100, - incomplete: '\u2591', - complete: '\u2588', - width: locales.length, - }); - let downloadingTotal = 0; - let downloadingReady = 0; +async function push(): Promise { + const { disapproveTranslates } = await prompt([{ + name: 'disapproveTranslates', + type: 'confirm', + default: true, + message: 'Disapprove changed lines?', + }]); - interface Result { - locale: ValuesType; - progress: number; - translatesFilePath: string; - } + console.log('Loading file info...'); + const fileId = await findFileId(CROWDIN_FILE_PATH); - const results = await Promise.all( - // TODO: there is should be some way to reimplement this - // with reduce to avoid null values - locales.map( - async (locale): Promise => { - const { files } = await crowdin.languageStatus(locale.code); - checkingProgressBar.tick(); - const fileInfo = findFile(files, CROWDIN_FILE_PATH); - - if (fileInfo === null) { - throw new Error('Unable to find translation file. Please check the CROWDIN_FILE_PATH param.'); - } - - const progress = (fileInfo.words_approved / fileInfo.words) * 100; - - if (!releasedLocales.includes(toInternalLocale(locale.code)) && progress < MIN_RELEASE_PROGRESS) { - return null; - } - - downloadingProgressBar.update(downloadingReady / ++downloadingTotal, { - cCurrent: downloadingReady, - cTotal: downloadingTotal, - }); - - const translatesFilePath = await crowdin.exportFile(CROWDIN_FILE_PATH, locale.code); - - downloadingProgressBar.update(++downloadingReady / downloadingTotal, { - cCurrent: downloadingReady, - cTotal: downloadingTotal, - }); - - return { - locale, - progress, - translatesFilePath, - }; - }, - ), + console.log('Uploading the source file to the storage...') + const { data: { id: storageId } } = await crowdin.uploadStorageApi.addStorage( + path.basename(CROWDIN_FILE_PATH), + fs.readFileSync(getLocaleFilePath(SOURCE_LANG)), ); - console.log('Locales are downloaded. Writing them to file system.'); - - const indexFileEntries: Record = { - en: { - code: 'en', - name: 'English', - englishName: 'English', - progress: 100, - isReleased: true, - }, - }; - await Promise.all( - results - .filter((result): result is Result => result !== null) - .map( - (result) => - new Promise((resolve, reject) => { - const { - locale: { code, name }, - progress, - translatesFilePath, - } = result; - const ourCode = toInternalLocale(code); - - indexFileEntries[ourCode] = { - code: ourCode, - name: NATIVE_NAMES_MAP[ourCode] || iso639.getNativeName(ourCode), - englishName: ENGLISH_NAMES_MAP[ourCode] || name, - progress: parseFloat(progress.toFixed(1)), - isReleased: releasedLocales.includes(ourCode), - }; - - fs.copyFile(translatesFilePath, path.join(LANG_DIR, `${ourCode}.json`), 0, (err) => { - err ? reject(err) : resolve(); - }); - }), - ), - ); - - console.log('Writing an index file.'); - - fs.writeFileSync(path.join(LANG_DIR, INDEX_FILE_NAME), serializeToModule(indexFileEntries)); - - console.log(ch.green('The index file was successfully written')); -} - -function push() { - return new Promise((resolve, reject) => { - prompt.start(); - prompt.get( - { - properties: { - disapprove: { - description: 'Disapprove changed lines? [Y/n]', - pattern: /^y|n$/i, - message: 'Please enter "y" or "n"', - default: 'y', - before: (value) => value.toLowerCase() === 'y', - }, - }, - }, - async (err, { disapprove }) => { - if (err) { - reject(err); - - return; - } - - console.log(`Publishing ${ch.bold(SOURCE_LANG)} translates file...`); - - await crowdin.updateFile( - { - [CROWDIN_FILE_PATH]: path.join(LANG_DIR, `${SOURCE_LANG}.json`), - }, - { - update_option: disapprove ? 'update_as_unapproved' : 'update_without_changes', - }, - ); - - console.log(ch.green('Success')); - - resolve(); - }, - ); + console.log(`Applying the new revision...`); + await crowdin.sourceFilesApi.updateOrRestoreFile(PROJECT_ID, fileId, { + storageId, + updateOption: disapproveTranslates + ? SourceFilesModel.UpdateOption.CLEAR_TRANSLATIONS_AND_APPROVALS + : SourceFilesModel.UpdateOption.KEEP_TRANSLATIONS_AND_APPROVALS, }); + + console.log(ch.green('Success')); } try { @@ -361,7 +240,7 @@ try { switch (action) { case 'pull': - pullNew(); + pull(); break; case 'push': push(); diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 14c3251..3db2cb0 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -12,20 +12,21 @@ "dependencies": { "@babel/node": "^7.8.3", "@crowdin/crowdin-api-client": "^1.8.0", - "@types/mkdirp": "^1.0.0", - "@types/progress": "^2.0.3", "axios": "^0.19.2", "chalk": "^4.0.0", "crowdin-api": "^4.0.0", "glob": "^7.1.6", + "inquirer": "^7.1.0", "iso-639-1": "^2.1.3", "json5": "^2.1.3", "mkdirp": "^1.0.4", - "multi-progress": "^2.0.0", - "prompt": "https://github.com/flatiron/prompt.git#master", - "utility-types": "^3.10.0" + "progress": "^2.0.3" }, "devDependencies": { + "@types/inquirer": "^6.5.0", + "@types/json5": "^0.0.30", + "@types/mkdirp": "^1.0.0", + "@types/progress": "^2.0.3", "@types/webpack": "^4.41.13" } } diff --git a/yarn.lock b/yarn.lock index dcbfd8b..5b25a2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3040,6 +3040,14 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880" integrity sha512-iYCgjm1dGPRuo12+BStjd1HiVQqhlRhWDOQigNxn023HcjnhsiFz9pc6CzJj4HwDCSQca9bxTL4PxJDbkdm3PA== +"@types/inquirer@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be" + integrity sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw== + dependencies: + "@types/through" "*" + rxjs "^6.4.0" + "@types/intl@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/intl/-/intl-1.2.0.tgz#1245511f13064402087979f498764611a3c758fc" @@ -3093,6 +3101,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== +"@types/json5@^0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818" + integrity sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -3332,6 +3345,13 @@ "@types/testing-library__dom" "*" pretty-format "^25.1.0" +"@types/through@*": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" + integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== + dependencies: + "@types/node" "*" + "@types/uglify-js@*": version "3.9.0" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.0.tgz#4490a140ca82aa855ad68093829e7fd6ae94ea87" @@ -4244,16 +4264,6 @@ async@~0.2.6: resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E= -async@~0.9.0: - version "0.9.2" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" - integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= - -async@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" - integrity sha1-+PwEyjoTeErenhZBr5hXjPvWR6k= - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -5576,11 +5586,6 @@ color@^3.0.0: color-convert "^1.9.1" color-string "^1.5.2" -colors@1.0.x: - version "1.0.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" - integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= - colors@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -6201,11 +6206,6 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" -cycle@1.0.x: - version "1.0.3" - resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" - integrity sha1-IegLK+hYD5i0aPN5QwZisEbDStI= - cyclist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" @@ -6349,11 +6349,6 @@ deep-equal@^1.0.1, deep-equal@^1.1.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" -deep-equal@~0.2.1: - version "0.2.2" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-0.2.2.tgz#84b745896f34c684e98f2ce0e42abaf43bba017d" - integrity sha1-hLdFiW80xoTpjyzg5Cq69Du6AX0= - deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -7402,11 +7397,6 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -eyes@0.1.x: - version "0.1.8" - resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" - integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A= - fake-tag@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/fake-tag/-/fake-tag-1.0.1.tgz#1d59da482240a02bd83500ca98976530ed154b0d" @@ -8613,11 +8603,6 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== -i@0.3.x: - version "0.3.6" - resolved "https://registry.yarnpkg.com/i/-/i-0.3.6.tgz#d96c92732076f072711b6b10fd7d4f65ad8ee23d" - integrity sha1-2WyScyB28HJxG2sQ/X1PZa2O4j0= - iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -8821,6 +8806,25 @@ inquirer@^7.0.0: strip-ansi "^5.1.0" through "^2.3.6" +inquirer@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29" + integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^3.0.0" + cli-cursor "^3.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.15" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.5.3" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + internal-ip@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" @@ -9387,7 +9391,7 @@ isomorphic-fetch@^2.1.1: node-fetch "^1.0.1" whatwg-fetch ">=0.10.0" -isstream@0.1.x, isstream@~0.1.2: +isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= @@ -10924,7 +10928,7 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@~0.5.1: +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -10975,13 +10979,6 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -multi-progress@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/multi-progress/-/multi-progress-2.0.0.tgz#29ccb42cf24874b1c6384f03127ce5dff7b22f2c" - integrity sha1-Kcy0LPJIdLHGOE8DEnzl3/eyLyw= - dependencies: - progress "^1.1.8" - multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" @@ -11000,7 +10997,7 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -mute-stream@0.0.8, mute-stream@~0.0.4: +mute-stream@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== @@ -11037,11 +11034,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -ncp@1.0.x: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ncp/-/ncp-1.0.1.tgz#d15367e5cb87432ba117d2bf80fdf45aecfb4246" - integrity sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY= - neatequal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/neatequal/-/neatequal-1.0.0.tgz#2ee1211bc9fa6e4c55715fd210bb05602eb1ae3b" @@ -12563,11 +12555,6 @@ progress-stream@^2.0.0: speedometer "~1.0.0" through2 "~2.0.3" -progress@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" - integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= - progress@^2.0.0, progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -12605,16 +12592,6 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -"prompt@https://github.com/flatiron/prompt.git#master": - version "1.0.0" - resolved "https://github.com/flatiron/prompt.git#8d5495c84c3f433b8f26ea2798f8ba68c3656459" - dependencies: - colors "^1.1.2" - read "1.0.x" - revalidator "0.1.x" - utile "0.3.x" - winston "2.x" - prompts@^2.0.1: version "2.3.0" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.0.tgz#a444e968fa4cc7e86689a74050685ac8006c4cc4" @@ -13203,13 +13180,6 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -read@1.0.x: - version "1.0.7" - resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" - integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ= - dependencies: - mute-stream "~0.0.4" - "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" @@ -13694,11 +13664,6 @@ retry@0.12.0, retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= -revalidator@0.1.x: - version "0.1.8" - resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b" - integrity sha1-/s5hv6DBtSoga9axgZgYS91SOjs= - rgb-regex@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" @@ -13709,7 +13674,7 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.3, rimraf@^2.7.1: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.3, rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -13750,6 +13715,11 @@ run-async@^2.2.0: dependencies: is-promise "^2.1.0" +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -14501,11 +14471,6 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== -stack-trace@0.0.x: - version "0.0.10" - resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" - integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= - stack-utils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593" @@ -15687,23 +15652,6 @@ utila@^0.4.0, utila@~0.4: resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= -utile@0.3.x: - version "0.3.0" - resolved "https://registry.yarnpkg.com/utile/-/utile-0.3.0.tgz#1352c340eb820e4d8ddba039a4fbfaa32ed4ef3a" - integrity sha1-E1LDQOuCDk2N26A5pPv6oy7U7zo= - dependencies: - async "~0.9.0" - deep-equal "~0.2.1" - i "0.3.x" - mkdirp "0.x.x" - ncp "1.0.x" - rimraf "2.x.x" - -utility-types@^3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" - integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== - utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -16203,18 +16151,6 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" -winston@2.x: - version "2.4.4" - resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.4.tgz#a01e4d1d0a103cf4eada6fc1f886b3110d71c34b" - integrity sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q== - dependencies: - async "~1.0.0" - colors "1.0.x" - cycle "1.0.x" - eyes "0.1.x" - isstream "0.1.x" - stack-trace "0.0.x" - word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" From 57cf6b377640f184dd7f5adfc1e2d4a58af40cc1 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Wed, 27 May 2020 19:06:44 +0300 Subject: [PATCH 03/21] Migrate i18n-collect to the new prompt library --- packages/scripts/i18n-collect.ts | 133 +++++++++++++++---------------- 1 file changed, 63 insertions(+), 70 deletions(-) diff --git a/packages/scripts/i18n-collect.ts b/packages/scripts/i18n-collect.ts index b2b82b3..1bede54 100644 --- a/packages/scripts/i18n-collect.ts +++ b/packages/scripts/i18n-collect.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import { sync as globSync } from 'glob'; import { sync as mkdirpSync } from 'mkdirp'; import chalk from 'chalk'; -import prompt from 'prompt'; +import { prompt } from 'inquirer'; import localesMap from 'app/i18n'; const MESSAGES_PATTERN = `${__dirname}/../../build/messages/**/*.json`; @@ -17,6 +17,55 @@ interface MessageDescriptor { defaultMessage: string; } +function buildLocales() { + mkdirpSync(LANG_DIR); + + SUPPORTED_LANGS.map((lang) => { + const destPath = `${LANG_DIR}/${lang}.json`; + const newMessages = readJSON>(destPath); + + keysToRename.forEach(([fromKey, toKey]) => { + newMessages[toKey] = newMessages[fromKey]; + delete newMessages[fromKey]; + }); + keysToRemove.forEach((key) => { + delete newMessages[key]; + }); + keysToUpdate.forEach((key) => { + newMessages[`--${key}`] = newMessages[key]; + newMessages[key] = collectedMessages[key]; + }); + keysToAdd.forEach((key) => { + newMessages[key] = collectedMessages[key]; + }); + + const sortedKeys: Array = Object.keys(newMessages).sort((key1, key2) => { + key1 = key1.replace(/^-+/, ''); + key2 = key2.replace(/^-+/, ''); + + return key1 < key2 || !isNotMarked(key1) ? -1 : 1; + }); + + const sortedNewMessages = sortedKeys.reduce((acc, key) => { + acc[key] = newMessages[key]; + + return acc; + }, {}); + + fs.writeFileSync(destPath, `${JSON.stringify(sortedNewMessages, null, 4)}\n`); + }); +} + +function readJSON(destPath: string): T { + try { + return JSON.parse(fs.readFileSync(destPath, 'utf8')); + } catch (err) { + console.log(chalk.yellow(`Can't read ${destPath}. The new file will be created.`), `(${err.message})`); + } + + return {} as T; +} + /** * Aggregates the default messages that were extracted from the app's * React components via the React Intl Babel plugin. An error will be thrown if @@ -117,77 +166,21 @@ if (keysToRename.length) { console.log(keysToRename.reduce((str, pair) => [str, pair[0], chalk.yellow(' -> '), pair[1], '\n'].join(''), '')); } -prompt.start(); -prompt.get( +prompt([ { - properties: { - apply: { - description: 'Apply changes? [Y/n]', - pattern: /^y|n$/i, - message: 'Please enter "y" or "n"', - default: 'y', - before: (value) => value.toLowerCase() === 'y', - }, - }, + type: 'confirm', + name: 'applyChanges', + message: 'Apply changes?', + default: true, }, - (err, resp) => { - console.log('\n'); +]).then(({ applyChanges }) => { + if (!applyChanges) { + console.log(chalk.red('Aborted')); - if (err || !resp.apply) { - return console.log(chalk.red('Aborted')); - } - - buildLocales(); - - console.log(chalk.green('All locales was successfuly built')); - }, -); - -function buildLocales() { - mkdirpSync(LANG_DIR); - - SUPPORTED_LANGS.map((lang) => { - const destPath = `${LANG_DIR}/${lang}.json`; - const newMessages = readJSON>(destPath); - - keysToRename.forEach(([fromKey, toKey]) => { - newMessages[toKey] = newMessages[fromKey]; - delete newMessages[fromKey]; - }); - keysToRemove.forEach((key) => { - delete newMessages[key]; - }); - keysToUpdate.forEach((key) => { - newMessages[`--${key}`] = newMessages[key]; - newMessages[key] = collectedMessages[key]; - }); - keysToAdd.forEach((key) => { - newMessages[key] = collectedMessages[key]; - }); - - const sortedKeys: Array = Object.keys(newMessages).sort((key1, key2) => { - key1 = key1.replace(/^-+/, ''); - key2 = key2.replace(/^-+/, ''); - - return key1 < key2 || !isNotMarked(key1) ? -1 : 1; - }); - - const sortedNewMessages = sortedKeys.reduce((acc, key) => { - acc[key] = newMessages[key]; - - return acc; - }, {}); - - fs.writeFileSync(destPath, `${JSON.stringify(sortedNewMessages, null, 4)}\n`); - }); -} - -function readJSON(destPath: string): T { - try { - return JSON.parse(fs.readFileSync(destPath, 'utf8')); - } catch (err) { - console.log(chalk.yellow(`Can't read ${destPath}. The new file will be created.`), `(${err.message})`); + return; } - return {} as T; -} + buildLocales(); + + console.log(chalk.green('All locales was successfuly built')); +}); From bf6a76d0060a02c1a6728a4e364fee91aaa618e8 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Thu, 4 Jun 2020 19:41:27 +0300 Subject: [PATCH 04/21] Remove all *.intl.json files. Move strings to the corresponding views. Implement custom command to build i18n/en.json file --- @types/react-intl.d.ts | 24 ++ babel.config.js | 102 +++--- package.json | 5 +- .../accounts/AccountSwitcher.intl.json | 5 - .../components/accounts/AccountSwitcher.tsx | 7 +- .../auth/acceptRules/AcceptRules.intl.json | 8 - .../auth/acceptRules/AcceptRules.ts | 8 +- .../auth/acceptRules/AcceptRulesBody.tsx | 13 +- .../auth/activation/Activation.intl.json | 8 - .../components/auth/activation/Activation.ts | 8 +- .../auth/activation/ActivationBody.tsx | 15 +- .../components/auth/appInfo/AppInfo.intl.json | 7 - .../app/components/auth/appInfo/AppInfo.tsx | 20 +- .../components/auth/appInfo/appName.intl.ts | 7 + .../chooseAccount/ChooseAccount.intl.json | 7 - .../auth/chooseAccount/ChooseAccount.ts | 8 +- .../auth/chooseAccount/ChooseAccountBody.tsx | 9 +- .../components/auth/finish/Finish.intl.json | 7 - .../app/components/auth/finish/Finish.tsx | 29 +- .../forgotPassword/ForgotPassword.intl.json | 7 - .../auth/forgotPassword/ForgotPassword.ts | 8 +- .../forgotPassword/ForgotPasswordBody.tsx | 19 +- .../app/components/auth/login/Login.intl.json | 6 - packages/app/components/auth/login/Login.ts | 8 +- .../app/components/auth/login/LoginBody.tsx | 5 +- .../app/components/auth/mfa/Mfa.intl.json | 4 - packages/app/components/auth/mfa/Mfa.tsx | 10 +- packages/app/components/auth/mfa/MfaBody.tsx | 12 +- .../auth/password/Password.intl.json | 7 - .../app/components/auth/password/Password.ts | 8 +- .../components/auth/password/PasswordBody.tsx | 7 +- .../auth/permissions/Permissions.intl.json | 12 - .../auth/permissions/Permissions.ts | 8 +- .../auth/permissions/PermissionsBody.tsx | 19 +- .../recoverPassword/RecoverPassword.intl.json | 12 - .../auth/recoverPassword/RecoverPassword.ts | 8 +- .../recoverPassword/RecoverPasswordBody.tsx | 30 +- .../auth/register/Register.intl.json | 10 - .../app/components/auth/register/Register.ts | 15 +- .../components/auth/register/RegisterBody.tsx | 24 +- .../ResendActivation.intl.json | 5 - .../auth/resendActivation/ResendActivation.ts | 11 +- .../resendActivation/ResendActivationBody.tsx | 15 +- .../app/components/contact/ContactForm.tsx | 53 ++- .../components/contact/contactForm.intl.json | 19 -- .../dev/apps/ApplicationsIndex.intl.json | 26 -- .../components/dev/apps/ApplicationsIndex.tsx | 35 +- .../applicationForm/ApplicationForm.intl.json | 20 -- .../apps/applicationForm/ApplicationForm.tsx | 19 +- .../applicationForm/MinecraftServerType.tsx | 11 +- .../dev/apps/applicationForm/WebsiteType.tsx | 15 +- packages/app/components/dev/apps/list.intl.ts | 24 ++ .../dev/apps/list/ApplicationItem.tsx | 2 +- .../dev/apps/list/ApplicationsList.tsx | 2 +- .../app/components/footerMenu/FooterMenu.tsx | 9 +- .../footerMenu/footerMenu.intl.json | 6 - .../languageSwitcher/LanguageList.tsx | 6 +- .../languageSwitcher/LanguageSwitcher.tsx | 20 +- .../languageSwitcher/LocaleItem.tsx | 6 +- .../languageSwitcher.intl.json | 10 - .../app/components/profile/Profile.intl.json | 19 -- packages/app/components/profile/Profile.tsx | 49 ++- .../components/profile/ProfileForm.intl.json | 3 - .../app/components/profile/ProfileForm.tsx | 11 +- .../profile/changeEmail/ChangeEmail.intl.json | 17 - .../profile/changeEmail/ChangeEmail.tsx | 50 ++- .../changePassword/ChangePassword.intl.json | 10 - .../profile/changePassword/ChangePassword.tsx | 35 +- .../changeUsername/ChangeUsername.intl.json | 6 - .../profile/changeUsername/ChangeUsername.tsx | 21 +- .../{confirmation => }/Confirmation.tsx | 11 +- .../profile/multiFactorAuth/MfaDisable.tsx | 4 +- .../{disableForm => }/MfaDisableForm.tsx | 17 +- .../profile/multiFactorAuth/MfaEnable.tsx | 14 +- .../{status => }/MfaStatus.tsx | 15 +- .../multiFactorAuth/MultiFactorAuth.intl.json | 25 -- .../multiFactorAuth/MultiFactorAuth.tsx | 8 +- .../multiFactorAuth/confirmation/index.ts | 1 - .../multiFactorAuth/instructions.intl.ts | 12 + .../instructions/Instructions.tsx | 2 +- .../instructions/OsInstruction.tsx | 2 +- .../profile/multiFactorAuth/keyForm.intl.ts | 12 + .../multiFactorAuth/keyForm/KeyForm.tsx | 2 +- .../PasswordRequestForm.intl.json | 5 - .../PasswordRequestForm.tsx | 13 +- .../app/components/ui/bsod/BSoD.intl.json | 5 - packages/app/components/ui/bsod/BSoD.tsx | 20 +- .../userbar/LoggedInPanel.intl.json | 3 - .../app/components/userbar/Userbar.intl.json | 4 - packages/app/components/userbar/Userbar.tsx | 5 +- packages/app/i18n/en.json | 6 + packages/app/pages/404/PageNotFound.intl.json | 6 - packages/app/pages/404/PageNotFound.tsx | 12 +- .../app/pages/auth/SuccessOauthPage.intl.json | 7 - packages/app/pages/auth/SuccessOauthPage.tsx | 24 +- packages/app/pages/root/RootPage.intl.json | 3 - packages/app/pages/root/RootPage.tsx | 4 +- packages/app/pages/root/siteName.intl.ts | 7 + packages/app/pages/rules/RulesPage.intl.json | 24 -- packages/app/pages/rules/RulesPage.tsx | 87 +++-- .../services/errorsDict/errorsDict.intl.json | 38 --- .../app/services/errorsDict/errorsDict.tsx | 76 ++--- packages/scripts/i18n-collect.ts | 186 ---------- packages/scripts/package.json | 2 - packages/webpack-utils/intl-loader.js | 38 --- webpack.config.js | 6 - yarn.lock | 317 ++++++++++++++++-- 107 files changed, 1122 insertions(+), 972 deletions(-) create mode 100644 @types/react-intl.d.ts delete mode 100644 packages/app/components/accounts/AccountSwitcher.intl.json delete mode 100644 packages/app/components/auth/acceptRules/AcceptRules.intl.json delete mode 100644 packages/app/components/auth/activation/Activation.intl.json delete mode 100644 packages/app/components/auth/appInfo/AppInfo.intl.json create mode 100644 packages/app/components/auth/appInfo/appName.intl.ts delete mode 100644 packages/app/components/auth/chooseAccount/ChooseAccount.intl.json delete mode 100644 packages/app/components/auth/finish/Finish.intl.json delete mode 100644 packages/app/components/auth/forgotPassword/ForgotPassword.intl.json delete mode 100644 packages/app/components/auth/login/Login.intl.json delete mode 100644 packages/app/components/auth/mfa/Mfa.intl.json delete mode 100644 packages/app/components/auth/password/Password.intl.json delete mode 100644 packages/app/components/auth/permissions/Permissions.intl.json delete mode 100644 packages/app/components/auth/recoverPassword/RecoverPassword.intl.json delete mode 100644 packages/app/components/auth/register/Register.intl.json delete mode 100644 packages/app/components/auth/resendActivation/ResendActivation.intl.json delete mode 100644 packages/app/components/contact/contactForm.intl.json delete mode 100644 packages/app/components/dev/apps/ApplicationsIndex.intl.json delete mode 100644 packages/app/components/dev/apps/applicationForm/ApplicationForm.intl.json create mode 100644 packages/app/components/dev/apps/list.intl.ts delete mode 100644 packages/app/components/footerMenu/footerMenu.intl.json delete mode 100644 packages/app/components/languageSwitcher/languageSwitcher.intl.json delete mode 100644 packages/app/components/profile/Profile.intl.json delete mode 100644 packages/app/components/profile/ProfileForm.intl.json delete mode 100644 packages/app/components/profile/changeEmail/ChangeEmail.intl.json delete mode 100644 packages/app/components/profile/changePassword/ChangePassword.intl.json delete mode 100644 packages/app/components/profile/changeUsername/ChangeUsername.intl.json rename packages/app/components/profile/multiFactorAuth/{confirmation => }/Confirmation.tsx (73%) rename packages/app/components/profile/multiFactorAuth/{disableForm => }/MfaDisableForm.tsx (68%) rename packages/app/components/profile/multiFactorAuth/{status => }/MfaStatus.tsx (68%) delete mode 100644 packages/app/components/profile/multiFactorAuth/MultiFactorAuth.intl.json delete mode 100644 packages/app/components/profile/multiFactorAuth/confirmation/index.ts create mode 100644 packages/app/components/profile/multiFactorAuth/instructions.intl.ts create mode 100644 packages/app/components/profile/multiFactorAuth/keyForm.intl.ts delete mode 100644 packages/app/components/profile/passwordRequestForm/PasswordRequestForm.intl.json delete mode 100644 packages/app/components/ui/bsod/BSoD.intl.json delete mode 100644 packages/app/components/userbar/LoggedInPanel.intl.json delete mode 100644 packages/app/components/userbar/Userbar.intl.json delete mode 100644 packages/app/pages/404/PageNotFound.intl.json delete mode 100644 packages/app/pages/auth/SuccessOauthPage.intl.json delete mode 100644 packages/app/pages/root/RootPage.intl.json create mode 100644 packages/app/pages/root/siteName.intl.ts delete mode 100644 packages/app/pages/rules/RulesPage.intl.json delete mode 100644 packages/app/services/errorsDict/errorsDict.intl.json delete mode 100644 packages/scripts/i18n-collect.ts delete mode 100644 packages/webpack-utils/intl-loader.js diff --git a/@types/react-intl.d.ts b/@types/react-intl.d.ts new file mode 100644 index 0000000..b7d88fb --- /dev/null +++ b/@types/react-intl.d.ts @@ -0,0 +1,24 @@ +import { PrimitiveType } from 'intl-messageformat'; +import { Formatters, IntlConfig, IntlFormatters, MessageDescriptor } from 'react-intl'; + +declare module 'react-intl' { + // Babel's plugin react-intl-auto always converts passed strings messages into the MessageDescriptor + export declare function defineMessages>( + msgs: U, + ): Record; + + // Babel's plugin react-intl-auto allows to call the formatMessage function with "key" field to automatically + // generate message's id + export interface KeyBasedMessageDescriptor { + key: string; + defaultMessage: string; + } + + export declare interface IntlShape extends IntlConfig, IntlFormatters { + formatters: Formatters; + formatMessage( + descriptor: MessageDescriptor | KeyBasedMessageDescriptor, + values?: Record, + ): string; + } +} diff --git a/babel.config.js b/babel.config.js index c95ccf7..93dd611 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,50 +1,62 @@ /* eslint-env node */ -module.exports = { - presets: [ - [ - '@babel/preset-typescript', - { - allowDeclareFields: true, - }, - ], - '@babel/preset-react', - [ - '@babel/preset-env', - { - ignoreBrowserslistConfig: true, - targets: { - node: true, +// @ts-nocheck +module.exports = function (api) { + api.cache(true); + + return { + presets: [ + [ + '@babel/preset-typescript', + { + allowDeclareFields: true, }, - modules: 'commonjs', - }, - ], - ], - plugins: [ - '@babel/plugin-syntax-dynamic-import', - '@babel/plugin-proposal-function-bind', - '@babel/plugin-proposal-class-properties', - [ - '@babel/plugin-transform-runtime', - { - corejs: 3, - }, - ], - ['react-intl', { messagesDir: './build/messages/' }], - ], - env: { - webpack: { - plugins: ['react-hot-loader/babel'], - presets: [ - [ - '@babel/preset-env', - { - ignoreBrowserslistConfig: false, - modules: false, - useBuiltIns: 'usage', // or "entry" - corejs: 3, - }, - ], ], + '@babel/preset-react', + [ + '@babel/preset-env', + { + ignoreBrowserslistConfig: true, + targets: { + node: true, + }, + modules: 'commonjs', + }, + ], + ], + plugins: [ + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-proposal-function-bind', + '@babel/plugin-proposal-class-properties', + [ + '@babel/plugin-transform-runtime', + { + corejs: 3, + }, + ], + [ + 'react-intl-auto', + { + removePrefix: 'packages.app', + messagesDir: './build/messages/', + useKey: true, + }, + ], + ], + env: { + webpack: { + plugins: ['react-hot-loader/babel'], + presets: [ + [ + '@babel/preset-env', + { + ignoreBrowserslistConfig: false, + modules: false, + useBuiltIns: 'usage', // or "entry" + corejs: 3, + }, + ], + ], + }, }, - }, + }; }; diff --git a/package.json b/package.json index 4d16a5c..8ded3bf 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "ts:check": "tsc", "ci:check": "yarn lint:check && yarn ts:check && yarn test", "analyze": "yarn run clean && yarn run build:webpack --analyze", - "i18n:collect": "babel-node --extensions \".ts\" ./packages/scripts/i18n-collect.ts", + "i18n:extract": "extract-messages -l=en -o ./packages/app/i18n --flat true 'packages/app/!(node_modules)/**/*.[tj]s?(x)'", "i18n:push": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts push", "i18n:pull": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts pull", "build": "yarn run clean && yarn run build:webpack", @@ -117,7 +117,7 @@ "@typescript-eslint/eslint-plugin": "^3.0.0", "@typescript-eslint/parser": "^3.0.0", "babel-loader": "^8.0.0", - "babel-plugin-react-intl": "^7.5.10", + "babel-plugin-react-intl-auto": "^3.3.0", "core-js": "3.6.5", "csp-webpack-plugin": "^2.0.2", "css-loader": "^3.5.3", @@ -130,6 +130,7 @@ "eslint-plugin-prettier": "^3.1.3", "eslint-plugin-react": "^7.20.0", "exports-loader": "^0.7.0", + "extract-react-intl-messages": "^4.1.1", "file-loader": "^6.0.0", "html-loader": "^1.1.0", "html-webpack-plugin": "^4.3.0", diff --git a/packages/app/components/accounts/AccountSwitcher.intl.json b/packages/app/components/accounts/AccountSwitcher.intl.json deleted file mode 100644 index e2ceb9c..0000000 --- a/packages/app/components/accounts/AccountSwitcher.intl.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "addAccount": "Add account", - "goToEly": "Go to Ely.by profile", - "logout": "Log out" -} diff --git a/packages/app/components/accounts/AccountSwitcher.tsx b/packages/app/components/accounts/AccountSwitcher.tsx index 60113fd..c39d712 100644 --- a/packages/app/components/accounts/AccountSwitcher.tsx +++ b/packages/app/components/accounts/AccountSwitcher.tsx @@ -11,7 +11,6 @@ import { getActiveAccount, Account } from 'app/components/accounts/reducer'; import { RootState } from 'app/reducers'; import styles from './accountSwitcher.scss'; -import messages from './AccountSwitcher.intl.json'; interface Props { switchAccount: (account: Account) => Promise; @@ -70,7 +69,7 @@ export class AccountSwitcher extends React.Component {
@@ -80,7 +79,7 @@ export class AccountSwitcher extends React.Component { onClick={this.onRemove(activeAccount)} href="#" > - +
@@ -127,7 +126,7 @@ export class AccountSwitcher extends React.Component { small className={styles.addAccount} label={ - + {(message) => (
diff --git a/packages/app/components/auth/acceptRules/AcceptRules.intl.json b/packages/app/components/auth/acceptRules/AcceptRules.intl.json deleted file mode 100644 index e2a0943..0000000 --- a/packages/app/components/auth/acceptRules/AcceptRules.intl.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "title": "User Agreement", - "accept": "Accept", - "declineAndLogout": "Decline and logout", - "description1": "We have updated our {link}.", - "termsOfService": "terms of service", - "description2": "In order to continue using {name} service, you need to accept them." -} diff --git a/packages/app/components/auth/acceptRules/AcceptRules.ts b/packages/app/components/auth/acceptRules/AcceptRules.ts index a8cd377..3d02480 100644 --- a/packages/app/components/auth/acceptRules/AcceptRules.ts +++ b/packages/app/components/auth/acceptRules/AcceptRules.ts @@ -1,6 +1,12 @@ +import { defineMessages } from 'react-intl'; import factory from '../factory'; import Body from './AcceptRulesBody'; -import messages from './AcceptRules.intl.json'; + +const messages = defineMessages({ + title: 'User Agreement', + accept: 'Accept', + declineAndLogout: 'Decline and logout', +}); export default factory({ title: messages.title, diff --git a/packages/app/components/auth/acceptRules/AcceptRulesBody.tsx b/packages/app/components/auth/acceptRules/AcceptRulesBody.tsx index c690545..371c7fc 100644 --- a/packages/app/components/auth/acceptRules/AcceptRulesBody.tsx +++ b/packages/app/components/auth/acceptRules/AcceptRulesBody.tsx @@ -5,10 +5,9 @@ import { Link } from 'react-router-dom'; import icons from 'app/components/ui/icons.scss'; import BaseAuthBody from 'app/components/auth/BaseAuthBody'; -import appInfo from 'app/components/auth/appInfo/AppInfo.intl.json'; +import appName from 'app/components/auth/appInfo/appName.intl'; import styles from './acceptRules.scss'; -import messages from './AcceptRules.intl.json'; export default class AcceptRulesBody extends BaseAuthBody { static displayName = 'AcceptRulesBody'; @@ -25,20 +24,22 @@ export default class AcceptRulesBody extends BaseAuthBody {

- + ), }} />
, + name: , }} />

diff --git a/packages/app/components/auth/activation/Activation.intl.json b/packages/app/components/auth/activation/Activation.intl.json deleted file mode 100644 index 0c5efd3..0000000 --- a/packages/app/components/auth/activation/Activation.intl.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "accountActivationTitle": "Account activation", - "activationMailWasSent": "Please check {email} for the message with further instructions", - "activationMailWasSentNoEmail": "Please check your E‑mail for the message with further instructions", - "confirmEmail": "Confirm E‑mail", - "didNotReceivedEmail": "Did not received E‑mail?", - "enterTheCode": "Enter the code from E‑mail here" -} diff --git a/packages/app/components/auth/activation/Activation.ts b/packages/app/components/auth/activation/Activation.ts index 9c2e737..48caced 100644 --- a/packages/app/components/auth/activation/Activation.ts +++ b/packages/app/components/auth/activation/Activation.ts @@ -1,7 +1,13 @@ +import { defineMessages } from 'react-intl'; import factory from '../factory'; -import messages from './Activation.intl.json'; import Body from './ActivationBody'; +const messages = defineMessages({ + accountActivationTitle: 'Account activation', + confirmEmail: 'Confirm E‑mail', + didNotReceivedEmail: 'Did not received E‑mail?', +}); + export default factory({ title: messages.accountActivationTitle, body: Body, diff --git a/packages/app/components/auth/activation/ActivationBody.tsx b/packages/app/components/auth/activation/ActivationBody.tsx index a27f4cb..dc4d4ba 100644 --- a/packages/app/components/auth/activation/ActivationBody.tsx +++ b/packages/app/components/auth/activation/ActivationBody.tsx @@ -1,12 +1,15 @@ import React from 'react'; -import { FormattedMessage as Message } from 'react-intl'; +import { defineMessages, FormattedMessage as Message } from 'react-intl'; import { Input } from 'app/components/ui/form'; import BaseAuthBody from 'app/components/auth/BaseAuthBody'; import styles from './activation.scss'; -import messages from './Activation.intl.json'; + +const messages = defineMessages({ + enterTheCode: 'Enter the code from E‑mail here', +}); export default class ActivationBody extends BaseAuthBody { static displayName = 'ActivationBody'; @@ -28,13 +31,17 @@ export default class ActivationBody extends BaseAuthBody {
{email ? ( {email}, }} /> ) : ( - + )}
diff --git a/packages/app/components/auth/appInfo/AppInfo.intl.json b/packages/app/components/auth/appInfo/AppInfo.intl.json deleted file mode 100644 index 1f25d49..0000000 --- a/packages/app/components/auth/appInfo/AppInfo.intl.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "appName": "Ely Accounts", - "goToAuth": "Go to auth", - "appDescription": "You are on the Ely.by authorization service, that allows you to safely perform any operations on your account. This single entry point for websites and desktop software, including game launchers.", - "useItYourself": "Visit our {link}, to learn how to use this service in you projects.", - "documentation": "documentation" -} diff --git a/packages/app/components/auth/appInfo/AppInfo.tsx b/packages/app/components/auth/appInfo/AppInfo.tsx index 004f819..06f9d33 100644 --- a/packages/app/components/auth/appInfo/AppInfo.tsx +++ b/packages/app/components/auth/appInfo/AppInfo.tsx @@ -1,10 +1,14 @@ import React from 'react'; -import { FormattedMessage as Message } from 'react-intl'; +import { defineMessages, FormattedMessage as Message } from 'react-intl'; import { Button } from 'app/components/ui/form'; import { FooterMenu } from 'app/components/footerMenu'; +import appName from './appName.intl'; import styles from './appInfo.scss'; -import messages from './AppInfo.intl.json'; + +const messages = defineMessages({ + goToAuth: 'Go to auth', +}); export default class AppInfo extends React.Component<{ name?: string; @@ -17,7 +21,7 @@ export default class AppInfo extends React.Component<{ return (
-

{name ? name : }

+

{name ? name : }

{description ? ( @@ -25,15 +29,19 @@ export default class AppInfo extends React.Component<{ ) : (

- +

- + ), }} diff --git a/packages/app/components/auth/appInfo/appName.intl.ts b/packages/app/components/auth/appInfo/appName.intl.ts new file mode 100644 index 0000000..b7a5cb9 --- /dev/null +++ b/packages/app/components/auth/appInfo/appName.intl.ts @@ -0,0 +1,7 @@ +import { defineMessages } from 'react-intl'; + +const { appName } = defineMessages({ + appName: 'Ely Accounts', +}); + +export default appName; diff --git a/packages/app/components/auth/chooseAccount/ChooseAccount.intl.json b/packages/app/components/auth/chooseAccount/ChooseAccount.intl.json deleted file mode 100644 index 1770688..0000000 --- a/packages/app/components/auth/chooseAccount/ChooseAccount.intl.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "chooseAccountTitle": "Choose an account", - "addAccount": "Log into another account", - "logoutAll": "Log out from all accounts", - "pleaseChooseAccount": "Please select an account you're willing to use", - "pleaseChooseAccountForApp": "Please select an account that you want to use to authorize {appName}" -} diff --git a/packages/app/components/auth/chooseAccount/ChooseAccount.ts b/packages/app/components/auth/chooseAccount/ChooseAccount.ts index cfa04ee..60e9272 100644 --- a/packages/app/components/auth/chooseAccount/ChooseAccount.ts +++ b/packages/app/components/auth/chooseAccount/ChooseAccount.ts @@ -1,7 +1,13 @@ +import { defineMessages } from 'react-intl'; import factory from '../factory'; -import messages from './ChooseAccount.intl.json'; import Body from './ChooseAccountBody'; +const messages = defineMessages({ + chooseAccountTitle: 'Choose an account', + addAccount: 'Log into another account', + logoutAll: 'Log out from all accounts', +}); + export default factory({ title: messages.chooseAccountTitle, body: Body, diff --git a/packages/app/components/auth/chooseAccount/ChooseAccountBody.tsx b/packages/app/components/auth/chooseAccount/ChooseAccountBody.tsx index 906e457..2361a9e 100644 --- a/packages/app/components/auth/chooseAccount/ChooseAccountBody.tsx +++ b/packages/app/components/auth/chooseAccount/ChooseAccountBody.tsx @@ -7,7 +7,6 @@ import { AccountSwitcher } from 'app/components/accounts'; import { Account } from 'app/components/accounts/reducer'; import styles from './chooseAccount.scss'; -import messages from './ChooseAccount.intl.json'; export default class ChooseAccountBody extends BaseAuthBody { static displayName = 'ChooseAccountBody'; @@ -23,14 +22,18 @@ export default class ChooseAccountBody extends BaseAuthBody {

{client ? ( {client.name}, }} /> ) : (
- +
)}
diff --git a/packages/app/components/auth/finish/Finish.intl.json b/packages/app/components/auth/finish/Finish.intl.json deleted file mode 100644 index a1aba85..0000000 --- a/packages/app/components/auth/finish/Finish.intl.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "authForAppSuccessful": "Authorization for {appName} was successfully completed", - "authForAppFailed": "Authorization for {appName} was failed", - "waitAppReaction": "Please, wait till your application response", - "passCodeToApp": "To complete authorization process, please, provide the following code to {appName}", - "copy": "Copy" -} diff --git a/packages/app/components/auth/finish/Finish.tsx b/packages/app/components/auth/finish/Finish.tsx index 19560a4..90078b4 100644 --- a/packages/app/components/auth/finish/Finish.tsx +++ b/packages/app/components/auth/finish/Finish.tsx @@ -1,14 +1,17 @@ import React, { MouseEventHandler } from 'react'; import { connect } from 'react-redux'; -import { FormattedMessage as Message } from 'react-intl'; +import { defineMessages, FormattedMessage as Message } from 'react-intl'; import { Helmet } from 'react-helmet-async'; import { Button } from 'app/components/ui/form'; import copy from 'app/services/copy'; import { RootState } from 'app/reducers'; -import messages from './Finish.intl.json'; import styles from './finish.scss'; +const messages = defineMessages({ + copy: 'Copy', +}); + interface Props { appName: string; code?: string; @@ -36,7 +39,8 @@ class Finish extends React.Component {
{appName}, }} @@ -45,7 +49,11 @@ class Finish extends React.Component { {displayCode ? (
- +
{code}
@@ -54,7 +62,10 @@ class Finish extends React.Component {
) : (
- +
)}
@@ -63,14 +74,18 @@ class Finish extends React.Component {
{appName}, }} />
- +
)} diff --git a/packages/app/components/auth/forgotPassword/ForgotPassword.intl.json b/packages/app/components/auth/forgotPassword/ForgotPassword.intl.json deleted file mode 100644 index 04eca46..0000000 --- a/packages/app/components/auth/forgotPassword/ForgotPassword.intl.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "Forgot password", - "sendMail": "Send mail", - "specifyEmail": "Specify the registration E‑mail address or last used username for your account and we will send an E‑mail with instructions for further password recovery.", - "pleasePressButton": "Please press the button bellow to get an E‑mail with password recovery code.", - "alreadyHaveCode": "Already have a code" -} diff --git a/packages/app/components/auth/forgotPassword/ForgotPassword.ts b/packages/app/components/auth/forgotPassword/ForgotPassword.ts index 79641fb..c4013a6 100644 --- a/packages/app/components/auth/forgotPassword/ForgotPassword.ts +++ b/packages/app/components/auth/forgotPassword/ForgotPassword.ts @@ -1,7 +1,13 @@ +import { defineMessages } from 'react-intl'; import factory from '../factory'; -import messages from './ForgotPassword.intl.json'; import Body from './ForgotPasswordBody'; +const messages = defineMessages({ + title: 'Forgot password', + sendMail: 'Send mail', + alreadyHaveCode: 'Already have a code', +}); + export default factory({ title: messages.title, body: Body, diff --git a/packages/app/components/auth/forgotPassword/ForgotPasswordBody.tsx b/packages/app/components/auth/forgotPassword/ForgotPasswordBody.tsx index 2dfc790..0a9745e 100644 --- a/packages/app/components/auth/forgotPassword/ForgotPasswordBody.tsx +++ b/packages/app/components/auth/forgotPassword/ForgotPasswordBody.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { FormattedMessage as Message } from 'react-intl'; +import { defineMessages, FormattedMessage as Message } from 'react-intl'; import { Input, Captcha } from 'app/components/ui/form'; import { getLogin } from 'app/components/auth/reducer'; @@ -8,7 +8,10 @@ import { PanelIcon } from 'app/components/ui/Panel'; import BaseAuthBody from 'app/components/auth/BaseAuthBody'; import styles from './forgotPassword.scss'; -import messages from './ForgotPassword.intl.json'; + +const messages = defineMessages({ + emailOrUsername: 'E‑mail or username', +}); export default class ForgotPasswordBody extends BaseAuthBody { static displayName = 'ForgotPasswordBody'; @@ -36,14 +39,17 @@ export default class ForgotPasswordBody extends BaseAuthBody { {isLoginEditShown ? (

- +

@@ -54,7 +60,10 @@ export default class ForgotPasswordBody extends BaseAuthBody {

- +

)} diff --git a/packages/app/components/auth/login/Login.intl.json b/packages/app/components/auth/login/Login.intl.json deleted file mode 100644 index 93f82eb..0000000 --- a/packages/app/components/auth/login/Login.intl.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "createNewAccount": "Create new account", - "loginTitle": "Sign in", - "emailOrUsername": "E‑mail or username", - "next": "Next" -} diff --git a/packages/app/components/auth/login/Login.ts b/packages/app/components/auth/login/Login.ts index 86a9575..e4134e2 100644 --- a/packages/app/components/auth/login/Login.ts +++ b/packages/app/components/auth/login/Login.ts @@ -1,6 +1,12 @@ +import { defineMessages } from 'react-intl'; import factory from '../factory'; import Body from './LoginBody'; -import messages from './Login.intl.json'; + +const messages = defineMessages({ + createNewAccount: 'Create new account', + loginTitle: 'Sign in', + next: 'Next', +}); export default factory({ title: messages.loginTitle, diff --git a/packages/app/components/auth/login/LoginBody.tsx b/packages/app/components/auth/login/LoginBody.tsx index 421efe5..c74302b 100644 --- a/packages/app/components/auth/login/LoginBody.tsx +++ b/packages/app/components/auth/login/LoginBody.tsx @@ -1,10 +1,13 @@ import React from 'react'; +import { defineMessages } from 'react-intl'; import { Input } from 'app/components/ui/form'; import BaseAuthBody from 'app/components/auth/BaseAuthBody'; import { User } from 'app/components/user/reducer'; -import messages from './Login.intl.json'; +const messages = defineMessages({ + emailOrUsername: 'E‑mail or username', +}); export default class LoginBody extends BaseAuthBody { static displayName = 'LoginBody'; diff --git a/packages/app/components/auth/mfa/Mfa.intl.json b/packages/app/components/auth/mfa/Mfa.intl.json deleted file mode 100644 index b19e7e8..0000000 --- a/packages/app/components/auth/mfa/Mfa.intl.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "enterTotp": "Enter code", - "description": "In order to sign in this account, you need to enter a one-time password from mobile application" -} diff --git a/packages/app/components/auth/mfa/Mfa.tsx b/packages/app/components/auth/mfa/Mfa.tsx index ffda313..350b5d6 100644 --- a/packages/app/components/auth/mfa/Mfa.tsx +++ b/packages/app/components/auth/mfa/Mfa.tsx @@ -1,13 +1,17 @@ +import { defineMessages } from 'react-intl'; import factory from '../factory'; import Body from './MfaBody'; -import messages from './Mfa.intl.json'; -import passwordMessages from '../password/Password.intl.json'; + +const messages = defineMessages({ + enterTotp: 'Enter code', + signInButton: 'Sign in', +}); export default factory({ title: messages.enterTotp, body: Body, footer: { color: 'green', - label: passwordMessages.signInButton, + label: messages.signInButton, }, }); diff --git a/packages/app/components/auth/mfa/MfaBody.tsx b/packages/app/components/auth/mfa/MfaBody.tsx index 02ea0f3..cb29216 100644 --- a/packages/app/components/auth/mfa/MfaBody.tsx +++ b/packages/app/components/auth/mfa/MfaBody.tsx @@ -1,11 +1,14 @@ import React from 'react'; -import { FormattedMessage as Message } from 'react-intl'; +import { defineMessages, FormattedMessage as Message } from 'react-intl'; import { PanelIcon } from 'app/components/ui/Panel'; import { Input } from 'app/components/ui/form'; import BaseAuthBody from 'app/components/auth/BaseAuthBody'; import styles from './mfa.scss'; -import messages from './Mfa.intl.json'; + +const messages = defineMessages({ + enterTotp: 'Enter code', +}); export default class MfaBody extends BaseAuthBody { static panelId = 'mfa'; @@ -21,7 +24,10 @@ export default class MfaBody extends BaseAuthBody {

- +

: }
- +
{user.username}
- +
- +
    {scopes.map((scope) => { const key = `scope_${scope}`; - const message = messages[key]; + // @ts-ignore + const message = scopesMessages[key]; return (
  • diff --git a/packages/app/components/auth/recoverPassword/RecoverPassword.intl.json b/packages/app/components/auth/recoverPassword/RecoverPassword.intl.json deleted file mode 100644 index ce776a5..0000000 --- a/packages/app/components/auth/recoverPassword/RecoverPassword.intl.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "title": "Restore password", - "contactSupport": "Contact support", - "messageWasSent": "The recovery code was sent to your account E‑mail.", - "messageWasSentTo": "The recovery code was sent to your E‑mail {email}.", - "enterCodeBelow": "Please enter the code received into the field below:", - "enterNewPasswordBelow": "Enter and repeat new password below:", - "change": "Change password", - "newPassword": "Enter new password", - "newRePassword": "Repeat new password", - "enterTheCode": "Enter confirmation code" -} diff --git a/packages/app/components/auth/recoverPassword/RecoverPassword.ts b/packages/app/components/auth/recoverPassword/RecoverPassword.ts index fb54884..aeaaffa 100644 --- a/packages/app/components/auth/recoverPassword/RecoverPassword.ts +++ b/packages/app/components/auth/recoverPassword/RecoverPassword.ts @@ -1,7 +1,13 @@ +import { defineMessages } from 'react-intl'; import factory from '../factory'; -import messages from './RecoverPassword.intl.json'; import Body from './RecoverPasswordBody'; +const messages = defineMessages({ + title: 'Restore password', + contactSupport: 'Contact support', + change: 'Change password', +}); + export default factory({ title: messages.title, body: Body, diff --git a/packages/app/components/auth/recoverPassword/RecoverPasswordBody.tsx b/packages/app/components/auth/recoverPassword/RecoverPasswordBody.tsx index e75a338..0389d95 100644 --- a/packages/app/components/auth/recoverPassword/RecoverPasswordBody.tsx +++ b/packages/app/components/auth/recoverPassword/RecoverPasswordBody.tsx @@ -1,15 +1,20 @@ import React from 'react'; -import { FormattedMessage as Message } from 'react-intl'; +import { defineMessages, FormattedMessage as Message } from 'react-intl'; import { Input } from 'app/components/ui/form'; import BaseAuthBody from 'app/components/auth/BaseAuthBody'; import styles from './recoverPassword.scss'; -import messages from './RecoverPassword.intl.json'; // TODO: activation code field may be decoupled into common component and reused here and in activation panel +const placeholders = defineMessages({ + newPassword: 'Enter new password', + newRePassword: 'Repeat new password', + enterTheCode: 'Enter confirmation code', +}); + export default class RecoverPasswordBody extends BaseAuthBody { static displayName = 'RecoverPasswordBody'; static panelId = 'recoverPassword'; @@ -28,15 +33,22 @@ export default class RecoverPasswordBody extends BaseAuthBody {

    {user.maskedEmail ? ( {user.maskedEmail}, }} /> ) : ( - + )}{' '} - +

    - +

); diff --git a/packages/app/components/auth/register/Register.intl.json b/packages/app/components/auth/register/Register.intl.json deleted file mode 100644 index 39e167b..0000000 --- a/packages/app/components/auth/register/Register.intl.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "registerTitle": "Sign Up", - "yourNickname": "Your nickname", - "yourEmail": "Your E‑mail", - "accountPassword": "Account password", - "repeatPassword": "Repeat password", - "signUpButton": "Register", - "acceptRules": "I agree with {link}", - "termsOfService": "terms of service" -} diff --git a/packages/app/components/auth/register/Register.ts b/packages/app/components/auth/register/Register.ts index 6536bd1..74c66e9 100644 --- a/packages/app/components/auth/register/Register.ts +++ b/packages/app/components/auth/register/Register.ts @@ -1,9 +1,14 @@ +import { defineMessages } from 'react-intl'; import factory from '../factory'; -import activationMessages from '../activation/Activation.intl.json'; -import forgotPasswordMessages from '../forgotPassword/ForgotPassword.intl.json'; -import messages from './Register.intl.json'; import Body from './RegisterBody'; +const messages = defineMessages({ + registerTitle: 'Sign Up', + signUpButton: 'Register', + didNotReceivedEmail: 'Did not received E‑mail?', + alreadyHaveCode: 'Already have a code', +}); + export default factory({ title: messages.registerTitle, body: Body, @@ -13,11 +18,11 @@ export default factory({ }, links: [ { - label: activationMessages.didNotReceivedEmail, + label: messages.didNotReceivedEmail, payload: { requestEmail: true }, }, { - label: forgotPasswordMessages.alreadyHaveCode, + label: messages.alreadyHaveCode, }, ], }); diff --git a/packages/app/components/auth/register/RegisterBody.tsx b/packages/app/components/auth/register/RegisterBody.tsx index 80aad09..7cc3a05 100644 --- a/packages/app/components/auth/register/RegisterBody.tsx +++ b/packages/app/components/auth/register/RegisterBody.tsx @@ -1,15 +1,20 @@ import React from 'react'; -import { FormattedMessage as Message } from 'react-intl'; +import { defineMessages, FormattedMessage as Message } from 'react-intl'; import { Link } from 'react-router-dom'; import { Input, Checkbox, Captcha } from 'app/components/ui/form'; import BaseAuthBody from 'app/components/auth/BaseAuthBody'; -import passwordMessages from '../password/Password.intl.json'; import styles from '../auth.scss'; -import messages from './Register.intl.json'; // TODO: password and username can be validate for length and sameness +const placeholders = defineMessages({ + yourNickname: 'Your nickname', + yourEmail: 'Your E‑mail', + accountPassword: 'Account password', + repeatPassword: 'Repeat password', +}); + export default class RegisterBody extends BaseAuthBody { static panelId = 'register'; @@ -26,7 +31,7 @@ export default class RegisterBody extends BaseAuthBody { color="blue" type="text" required - placeholder={messages.yourNickname} + placeholder={placeholders.yourNickname} /> @@ -65,11 +70,12 @@ export default class RegisterBody extends BaseAuthBody { required label={ - + ), }} diff --git a/packages/app/components/auth/resendActivation/ResendActivation.intl.json b/packages/app/components/auth/resendActivation/ResendActivation.intl.json deleted file mode 100644 index b2d7652..0000000 --- a/packages/app/components/auth/resendActivation/ResendActivation.intl.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Did not received an E‑mail", - "specifyYourEmail": "Please, enter an E‑mail you've registered with and we will send you new activation code", - "sendNewEmail": "Send new E‑mail" -} diff --git a/packages/app/components/auth/resendActivation/ResendActivation.ts b/packages/app/components/auth/resendActivation/ResendActivation.ts index d56a772..f283fa5 100644 --- a/packages/app/components/auth/resendActivation/ResendActivation.ts +++ b/packages/app/components/auth/resendActivation/ResendActivation.ts @@ -1,8 +1,13 @@ +import { defineMessages } from 'react-intl'; import factory from '../factory'; -import forgotPasswordMessages from '../forgotPassword/ForgotPassword.intl.json'; -import messages from './ResendActivation.intl.json'; import Body from './ResendActivationBody'; +const messages = defineMessages({ + title: 'Did not received an E‑mail', + sendNewEmail: 'Send new E‑mail', + alreadyHaveCode: 'Already have a code', +}); + export default factory({ title: messages.title, body: Body, @@ -11,6 +16,6 @@ export default factory({ label: messages.sendNewEmail, }, links: { - label: forgotPasswordMessages.alreadyHaveCode, + label: messages.alreadyHaveCode, }, }); diff --git a/packages/app/components/auth/resendActivation/ResendActivationBody.tsx b/packages/app/components/auth/resendActivation/ResendActivationBody.tsx index 9fd24cd..c468515 100644 --- a/packages/app/components/auth/resendActivation/ResendActivationBody.tsx +++ b/packages/app/components/auth/resendActivation/ResendActivationBody.tsx @@ -1,11 +1,13 @@ import React from 'react'; -import { FormattedMessage as Message } from 'react-intl'; +import { defineMessages, FormattedMessage as Message } from 'react-intl'; import { Input, Captcha } from 'app/components/ui/form'; import BaseAuthBody from '../BaseAuthBody'; -import registerMessages from '../register/Register.intl.json'; import styles from './resendActivation.scss'; -import messages from './ResendActivation.intl.json'; + +const placeholders = defineMessages({ + yourEmail: 'Your E‑mail', +}); export default class ResendActivation extends BaseAuthBody { static displayName = 'ResendActivation'; @@ -20,7 +22,10 @@ export default class ResendActivation extends BaseAuthBody { {this.renderErrors()}
- +
diff --git a/packages/app/components/contact/ContactForm.tsx b/packages/app/components/contact/ContactForm.tsx index 2bbe241..0fff30d 100644 --- a/packages/app/components/contact/ContactForm.tsx +++ b/packages/app/components/contact/ContactForm.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import clsx from 'clsx'; -import { FormattedMessage as Message } from 'react-intl'; +import { FormattedMessage as Message, defineMessages } from 'react-intl'; import { Input, TextArea, Button, Form, FormModel, Dropdown } from 'app/components/ui/form'; import feedback from 'app/services/api/feedback'; import icons from 'app/components/ui/icons.scss'; @@ -11,17 +11,27 @@ import logger from 'app/services/logger'; import { User } from 'app/components/user'; import styles from './contactForm.scss'; -import messages from './contactForm.intl.json'; const CONTACT_CATEGORIES = { // TODO: сюда позже проставить реальные id категорий с backend - 0: , - 1: , - 2: , - 3: , - 4: , + 0: , + 1: , + 2: , + 3: , + 4: , }; +const labels = defineMessages({ + subject: 'Subject', + email: 'E‑mail', + message: 'Message', + whichQuestion: 'What are you interested in?', + + send: 'Send', + + close: 'Close', +}); + export class ContactForm extends React.Component< { onClose: () => void; @@ -54,7 +64,7 @@ export class ContactForm extends React.Component<

- +

- +
- +
- +
@@ -115,7 +131,7 @@ export class ContactForm extends React.Component<