From 41e3239ec40ac2b9ecc0652ccb1864f7bed0f41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=92scar=20Casajuana?= Date: Wed, 27 Nov 2024 18:05:58 +0100 Subject: [PATCH] integrating process features with plan permissions - Trying to get rid of the plans mock, but it still needs some work (complicated when most features are missing from the plan rn) - Commented all the non implemented features in the process create form --- src/components/Account/useAccountPlan.tsx | 74 --------------- src/components/Dashboard/ProcessView.tsx | 2 +- src/components/Pricing/Card.tsx | 33 +------ src/components/Pricing/Features.tsx | 94 +++++++++++++++++++ src/components/Pricing/Upgrading.tsx | 9 +- .../Questions/useSaasVotingType.ts | 82 ---------------- src/components/ProcessCreate/SaasFooter.tsx | 24 ++--- .../ProcessCreate/Settings/SaasFeatures.tsx | 86 ++++++++--------- .../ProcessCreate/StepForm/Questions.tsx | 41 +++----- .../ProcessCreate/StepForm/SaasFeatures.tsx | 4 +- .../ProcessCreate/Steps/Confirm.tsx | 4 +- src/i18n/locales/ca.json | 58 +++++------- src/i18n/locales/en.json | 56 +++++------ src/i18n/locales/es.json | 56 +++++------ 14 files changed, 244 insertions(+), 379 deletions(-) delete mode 100644 src/components/Account/useAccountPlan.tsx create mode 100644 src/components/Pricing/Features.tsx delete mode 100644 src/components/ProcessCreate/Questions/useSaasVotingType.ts diff --git a/src/components/Account/useAccountPlan.tsx b/src/components/Account/useAccountPlan.tsx deleted file mode 100644 index b8ca2e0cf..000000000 --- a/src/components/Account/useAccountPlan.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { UnimplementedVotingType } from '~components/ProcessCreate/Questions/useUnimplementedVotingType' -import { VotingType } from '~components/ProcessCreate/Questions/useVotingType' - -type PlanType = 'free' | 'pro' | 'custom' - -export type FeaturesKeys = - | 'anonymous' - | 'secretUntilTheEnd' - | 'overwrite' - | 'personalization' - | 'emailReminder' - | 'smsNotification' - | 'whiteLabel' - | 'liveStreaming' -export type SaasVotingTypesKeys = VotingType & UnimplementedVotingType - -type SaasOrganizationInfo = { - memberships: number - subOrgs: number - maxProcesses: number - max_census_size: number - customURL: boolean -} - -type AccountPlanTypeResponse = { - plan: PlanType - stripePlanId: string - organization: SaasOrganizationInfo - votingTypes: Record - features: Record -} - -const accountPlanMock: AccountPlanTypeResponse = { - plan: 'pro', - stripePlanId: 'plan_xyz123', // Stripe plan ID for payment and plan management - organization: { - memberships: 5, // Maximum number of members or admins - subOrgs: 3, // Maximum number of sub-organizations - maxProcesses: 10, // Maximum number of voting processes - max_census_size: 10000, // Maximum number of voters (census size) for this plan - customURL: true, // Whether a custom URL for the voting page is allowed - }, - votingTypes: { - single: true, // Simple single-choice voting allowed - multiple: true, // Multiple-choice voting allowed - approval: true, // Approval voting allowed - cumulative: false, // Cumulative voting not allowed - ranked: false, // Ranked voting not allowed - weighted: false, // Weighted voting not allowed - }, - features: { - personalization: true, // Voting page customization allowed - emailReminder: true, // Email reminders allowed - smsNotification: true, // SMS notifications allowed - whiteLabel: true, // White-label voting page allowed - liveStreaming: false, // Live results streaming not allowed - anonymous: true, - secretUntilTheEnd: true, - overwrite: true, - // ... Other feature controls - }, -} - -export const useAccountPlan = () => ({ - data: accountPlanMock, -}) -// this was the cod before refactoring themes and features, but it was not working -// useQuery({ -// queryKey: ['account', 'plan'], -// queryFn: async () => { -// // Simulate an API call -// return accountPlanMock -// }, -// }) diff --git a/src/components/Dashboard/ProcessView.tsx b/src/components/Dashboard/ProcessView.tsx index bfc0de7cb..9696a9c20 100644 --- a/src/components/Dashboard/ProcessView.tsx +++ b/src/components/Dashboard/ProcessView.tsx @@ -209,7 +209,7 @@ export const ProcessView = () => { {/* Features Section */} - Features + Features Feature 1 diff --git a/src/components/Pricing/Card.tsx b/src/components/Pricing/Card.tsx index 68446dc1b..b9d521827 100644 --- a/src/components/Pricing/Card.tsx +++ b/src/components/Pricing/Card.tsx @@ -15,7 +15,8 @@ import { useDisclosure, } from '@chakra-ui/react' import { dotobject } from '@vocdoni/sdk' -import { Trans, useTranslation } from 'react-i18next' +import { Trans } from 'react-i18next' +import { PlanFeaturesTranslationKeys } from './Features' import type { Plan } from './Plans' type PricingCardProps = { @@ -29,18 +30,6 @@ type PricingCardProps = { plan: Plan } -// Translation keys for the features -const translations = { - 'organization.memberships': 'pricing.features.memberships', - 'organization.subOrgs': 'pricing.features.sub_orgs', - 'votingTypes.weighted': 'pricing.features.weighted', - 'votingTypes.approval': 'pricing.features.approval', - 'votingTypes.ranked': 'pricing.features.ranked', - 'features.personalization': 'pricing.features.personalization', - 'features.emailReminder': 'pricing.features.email_reminder', - 'features.smsNotification': 'pricing.features.sms_notification', -} - const PricingCard = ({ title, subtitle, @@ -54,7 +43,7 @@ const PricingCard = ({ const { isOpen, onToggle } = useDisclosure() // Dynamically map the features from the plan - const features = Object.entries(translations) + const features = Object.entries(PlanFeaturesTranslationKeys) .map(([key, translationKey]) => { const value = dotobject(plan, key) return value !== undefined @@ -138,20 +127,4 @@ const PricingCard = ({ ) } -// yeah, it's sad but we need to include all the translations in a way the extractor does not remove them... -// note this component does not need (and should never) to be included in the app -const UnusedComponentButRequiredToNotLoseTranslations = () => { - const { t } = useTranslation() - t('pricing.features.memberships', { defaultValue: 'Up to {{ count }} memberships' }) - t('pricing.features.sub_orgs', { defaultValue: 'Up to {{ count }} sub-organizations' }) - t('pricing.features.approval', { defaultValue: 'Approval voting' }) - t('pricing.features.ranked', { defaultValue: 'Ranked voting' }) - t('pricing.features.weighted', { defaultValue: 'Weighted voting' }) - t('pricing.features.personalization', { defaultValue: 'Personalization' }) - t('pricing.features.email_reminder', { defaultValue: 'Email reminder' }) - t('pricing.features.sms_notification', { defaultValue: 'SMS notification' }) - - return null -} - export default PricingCard diff --git a/src/components/Pricing/Features.tsx b/src/components/Pricing/Features.tsx new file mode 100644 index 000000000..4181b777c --- /dev/null +++ b/src/components/Pricing/Features.tsx @@ -0,0 +1,94 @@ +import { dotobject } from '@vocdoni/sdk' +import { useTranslation } from 'react-i18next' +import type { Plan } from './Plans' + +export type FeaturesKeys = + | 'anonymous' + | 'secretUntilTheEnd' + | 'overwrite' + | 'personalization' + | 'emailReminder' + | 'smsNotification' + | 'whiteLabel' + | 'liveStreaming' + +// Translation keys for the subscription features +export const PlanFeaturesTranslationKeys = { + 'organization.memberships': 'features.memberships', + 'organization.subOrgs': 'features.sub_orgs', + 'votingTypes.weighted': 'features.weighted', + 'votingTypes.approval': 'features.approval', + 'votingTypes.ranked': 'features.ranked', + 'features.personalization': 'features.personalization', + 'features.emailReminder': 'features.email_reminder', + 'features.smsNotification': 'features.sms_notification', +} + +/** + * Checks if a given feature exists and meets the required condition in a plan. + * + * @param plan - The plan object to check. + * @param featurePath - Dot notation path to the feature (e.g., 'organization.memberships'). + * @param expectedValue - Expected value or comparison object. + * - If a number, checks for >= comparison. + * - If an object, supports { operator, value } (e.g., { operator: '>=', value: 10 }). + * @returns boolean - `true` if the feature meets the condition, `false` otherwise. + */ +export const isFeatureAvailable = ( + plan: Plan, + featurePath: string, + expectedValue?: number | { operator: '===' | '>' | '>=' | '<' | '<='; value: number } +): boolean => { + const featureValue = dotobject(plan, featurePath) // Get the feature value using dot notation + + if (typeof featureValue === 'undefined') { + return false // If the feature doesn't exist, return false + } + + // If no expected value is provided, return true if the feature exists + if (typeof expectedValue === 'undefined') { + return true + } + + // Handle exact match or comparison + if (typeof expectedValue === 'number') { + return featureValue >= expectedValue // Default to "greater than or equal to" for numbers + } + + if (typeof expectedValue === 'object' && expectedValue.operator && expectedValue.value !== undefined) { + const { operator, value } = expectedValue + + switch (operator) { + case '===': + return featureValue === value + case '>': + return featureValue > value + case '>=': + return featureValue >= value + case '<': + return featureValue < value + case '<=': + return featureValue <= value + default: + throw new Error(`Unsupported operator: ${operator}`) + } + } + + throw new Error('Invalid expectedValue type') +} + +// yeah, it's sad but we need to include all the translations in a way the extractor does not remove them... +// note this component does not need (and should never) to be included in the app +const UnusedComponentButRequiredToNotLoseTranslations = () => { + const { t } = useTranslation() + t('features.memberships', { defaultValue: 'Up to {{ count }} memberships' }) + t('features.sub_orgs', { defaultValue: 'Up to {{ count }} sub-organizations' }) + t('features.approval', { defaultValue: 'Approval voting' }) + t('features.ranked', { defaultValue: 'Ranked voting' }) + t('features.weighted', { defaultValue: 'Weighted voting' }) + t('features.personalization', { defaultValue: 'Personalization' }) + t('features.email_reminder', { defaultValue: 'Email reminder' }) + t('features.sms_notification', { defaultValue: 'SMS notification' }) + + return null +} diff --git a/src/components/Pricing/Upgrading.tsx b/src/components/Pricing/Upgrading.tsx index da90c26b5..1258b2ca0 100644 --- a/src/components/Pricing/Upgrading.tsx +++ b/src/components/Pricing/Upgrading.tsx @@ -109,18 +109,17 @@ export const PlanUpgrade = ({ feature, text, value }: PlanUpgradeData) => { {isLoading && } - {plans?.map((plan) => ( - + {plans?.map((plan, key) => ( + - {(!value && isFeatureAvailable(plan, feature)) || - (value && isFeatureAvailable(plan, feature, value)) ? ( + {isFeatureAvailable(plan, feature, value) ? ( ) : ( )} {translations[plan.id].title} - + {key < plans.length - 1 && } ))} diff --git a/src/components/ProcessCreate/Questions/useSaasVotingType.ts b/src/components/ProcessCreate/Questions/useSaasVotingType.ts deleted file mode 100644 index 9b387fc28..000000000 --- a/src/components/ProcessCreate/Questions/useSaasVotingType.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { GiChoice } from 'react-icons/gi' -import { SaasVotingTypesKeys, useAccountPlan } from '~components/Account/useAccountPlan' -import SingleChoice from '~components/ProcessCreate/Questions/SingleChoice' -import { GenericFeatureObject, GenericFeatureObjectProps } from '~components/ProcessCreate/Steps/TabsPage' - -const useVotingTypesTranslations = (): Record => { - const { t } = useTranslation() - return useMemo( - () => ({ - single: { - description: t('single', { defaultValue: 'single' }), - title: t('single', { defaultValue: 'single' }), - icon: GiChoice, - component: SingleChoice, - }, - multiple: { - description: t('multiple', { defaultValue: 'multiple' }), - title: t('multiple', { defaultValue: 'multiple' }), - icon: GiChoice, - component: SingleChoice, - }, - approval: { - description: t('approval', { defaultValue: 'approval' }), - title: t('approval', { defaultValue: 'approval' }), - icon: GiChoice, - component: SingleChoice, - }, - cumulative: { - description: t('cumulative', { defaultValue: 'cumulative' }), - title: t('cumulative', { defaultValue: 'cumulative' }), - icon: GiChoice, - component: SingleChoice, - }, - ranked: { - description: t('ranked', { defaultValue: 'ranked' }), - title: t('ranked', { defaultValue: 'ranked' }), - icon: GiChoice, - component: SingleChoice, - }, - weighted: { - description: t('weighted', { defaultValue: 'weighted' }), - title: t('weighted', { defaultValue: 'weighted' }), - icon: GiChoice, - component: SingleChoice, - }, - }), - [t] - ) -} - -export const useSaasVotingType = (): { - inPlan: GenericFeatureObject> - pro: GenericFeatureObject> -} => { - const { data } = useAccountPlan() - const translations = useVotingTypesTranslations() - - const inPlanDetails = {} as Record, GenericFeatureObjectProps> - const proDetails = {} as Record, GenericFeatureObjectProps> - - for (const [key, inPlan] of Object.entries(data.votingTypes)) { - const _key = key as SaasVotingTypesKeys - if (inPlan) { - inPlanDetails[_key] = translations[_key] - continue - } - proDetails[_key] = translations[_key] - } - - return { - inPlan: { - defined: Object.keys(inPlanDetails) as Partial[], - details: inPlanDetails, - }, - pro: { - defined: Object.keys(proDetails) as Partial[], - details: proDetails, - }, - } -} diff --git a/src/components/ProcessCreate/SaasFooter.tsx b/src/components/ProcessCreate/SaasFooter.tsx index 87293e4db..ca5d930aa 100644 --- a/src/components/ProcessCreate/SaasFooter.tsx +++ b/src/components/ProcessCreate/SaasFooter.tsx @@ -1,15 +1,15 @@ -import { Box, Flex, Link, Text } from '@chakra-ui/react' -import { Button } from '@vocdoni/chakra-components' +import { Box, Flex, Link } from '@chakra-ui/react' import { useTranslation } from 'react-i18next' import { Link as ReactRouterLink } from 'react-router-dom' -import { useAccountPlan } from '~components/Account/useAccountPlan' +import { useSubscription } from '~components/Auth/Subscription' import { VocdoniLogo } from '~components/Layout/Logo' +import { PlanId } from '~constants' const SaasFooter = () => { const { t } = useTranslation() - const { data } = useAccountPlan() - const isCustom = data?.plan === 'custom' - const isFree = data?.plan === 'free' + const { subscription } = useSubscription() + const isCustom = subscription?.plan.id === PlanId.Custom + const isFree = subscription?.plan.id === PlanId.Free return ( @@ -35,17 +35,7 @@ const SaasFooter = () => { {t('privacy_policy', { defaultValue: 'Privacy Policy' })} - - support@vocdoni.org - - - - {isFree && ( - - $0.00 - - )} - {!isCustom && } + support@vocdoni.org diff --git a/src/components/ProcessCreate/Settings/SaasFeatures.tsx b/src/components/ProcessCreate/Settings/SaasFeatures.tsx index 1585b0523..46f31f1ac 100644 --- a/src/components/ProcessCreate/Settings/SaasFeatures.tsx +++ b/src/components/ProcessCreate/Settings/SaasFeatures.tsx @@ -4,71 +4,71 @@ import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { IconType } from 'react-icons' import { BiCheckDouble } from 'react-icons/bi' -import { FeaturesKeys, useAccountPlan } from '~components/Account/useAccountPlan' +import { useSubscription } from '~components/Auth/Subscription' -const useFeaturesTranslations = (): Record => { +const useProcessFeatures = () => { const { t } = useTranslation() return useMemo( () => ({ anonymous: { - description: t('anonymous', { defaultValue: 'anonymous' }), - title: t('anonymous', { defaultValue: 'anonymous' }), + title: t('anonymous.title', { defaultValue: 'Anonymous' }), + description: t('anonymous.description', { defaultValue: 'Voters will remain anonymous' }), boxIcon: BiCheckDouble, formKey: 'electionType.anonymous', }, secretUntilTheEnd: { - description: t('secretUntilTheEnd', { defaultValue: 'secretUntilTheEnd' }), - title: t('secretUntilTheEnd', { defaultValue: 'secretUntilTheEnd' }), + title: t('secret_until_the_end.title', { defaultValue: 'Secret until the end' }), + description: t('secret_until_the_end.description', { + defaultValue: 'Vote contents will be encrypted till the end of the voting', + }), boxIcon: BiCheckDouble, formKey: 'electionType.secretUntilTheEnd', }, overwrite: { - description: t('overwrite', { defaultValue: 'overwrite' }), - title: t('overwrite', { defaultValue: 'overwrite' }), - boxIcon: BiCheckDouble, - }, - personalization: { - description: t('personalization', { defaultValue: 'personalization' }), - title: t('personalization', { defaultValue: 'personalization' }), - boxIcon: BiCheckDouble, - }, - emailReminder: { - description: t('emailReminder', { defaultValue: 'emailReminder' }), - title: t('emailReminder', { defaultValue: 'emailReminder' }), - boxIcon: BiCheckDouble, - }, - smsNotification: { - description: t('smsNotification', { defaultValue: 'smsNotification' }), - title: t('smsNotification', { defaultValue: 'smsNotification' }), - boxIcon: BiCheckDouble, - }, - whiteLabel: { - description: t('whiteLabel', { defaultValue: 'whiteLabel' }), - title: t('whiteLabel', { defaultValue: 'whiteLabel' }), - boxIcon: BiCheckDouble, - }, - liveStreaming: { - description: t('liveStreaming', { defaultValue: 'liveStreaming' }), - title: t('liveStreaming', { defaultValue: 'liveStreaming' }), + title: t('overwrite.title', { defaultValue: 'Vote overwrite' }), + description: t('overwrite.description', { defaultValue: 'Voters will be able to overwrite their vote once' }), + formKey: 'maxVoteOverwrites', boxIcon: BiCheckDouble, }, + // non implemented features... + // personalization: { + // title: t('personalization.title', { defaultValue: 'personalization' }), + // description: t('personalization.description', { defaultValue: 'personalization' }), + // boxIcon: BiCheckDouble, + // }, + // emailReminder: { + // title: t('email_reminder.title', { defaultValue: 'Email reminder' }), + // description: t('email_reminder.description', { defaultValue: 'Remind by email' }), + // boxIcon: BiCheckDouble, + // }, + // smsNotification: { + // title: t('sms_notification.title', { defaultValue: 'SMS Notification' }), + // description: t('sms_notification.description', { defaultValue: 'Notify users somehow (?)' }), + // boxIcon: BiCheckDouble, + // }, + // whiteLabel: { + // title: t('white_label.title', { defaultValue: 'White label' }), + // description: t('white_label.description', { defaultValue: 'Customize the process layout entirely adding your own logos and color palette' }), + // boxIcon: BiCheckDouble, + // }, + // liveStreaming: { + // title: t('live_streaming.title', { defaultValue: 'Live Streaming' }), + // description: t('live_streaming.description', { defaultValue: 'IDK what\'s this about' }), + // boxIcon: BiCheckDouble, + // }, }), [t] ) } -export const SaasFeatures = () => { - const { data } = useAccountPlan() - const translations = useFeaturesTranslations() - - if (!data) return null +export const Features = () => { + const { subscription } = useSubscription() + const translations = useProcessFeatures() return ( - {Object.entries(data.features).map(([feature, inPlan], i) => { - const card = translations[feature as FeaturesKeys] - if (!card) return null - return - })} + {Object.entries(translations).map(([feature, card], i) => ( + + ))} ) } diff --git a/src/components/ProcessCreate/StepForm/Questions.tsx b/src/components/ProcessCreate/StepForm/Questions.tsx index f89d32c70..b2e570f46 100644 --- a/src/components/ProcessCreate/StepForm/Questions.tsx +++ b/src/components/ProcessCreate/StepForm/Questions.tsx @@ -1,9 +1,9 @@ -import { Flex } from '@chakra-ui/react' +import { Box } from '@chakra-ui/react' import { FormProvider, SubmitHandler, useFieldArray, useForm, useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import { useSaasVotingType } from '~components/ProcessCreate/Questions/useSaasVotingType' -import { MultiQuestionTypes, VotingType } from '~components/ProcessCreate/Questions/useVotingType' -import { ITabsPageProps, TabsPage } from '~components/ProcessCreate/Steps/TabsPage' +import { useUnimplementedVotingType } from '~components/ProcessCreate/Questions/useUnimplementedVotingType' +import { MultiQuestionTypes, useVotingType, VotingType } from '~components/ProcessCreate/Questions/useVotingType' +import { TabsPage } from '~components/ProcessCreate/Steps/TabsPage' import { StepsFormValues, useProcessCreationSteps } from '../Steps/use-steps' export interface Option { @@ -21,18 +21,12 @@ export interface QuestionsValues { questionType: VotingType | null } -const SaasQuestionsTabs = () => { - const { pro, inPlan } = useSaasVotingType() - return -} - -const QuestionsTabs = ({ - definedList, - unimplementedList, -}: Pick, 'definedList' | 'unimplementedList'>) => { +const QuestionsTabs = () => { const { t } = useTranslation() const { form, setForm } = useProcessCreationSteps() + const definedVotingTypes = useVotingType() + const unDefinedVotingTypes = useUnimplementedVotingType() const { questionType } = form const { watch } = useFormContext() @@ -45,17 +39,17 @@ const QuestionsTabs = return ( { - const newQuestionType = definedList.defined[index] + const newQuestionType = definedVotingTypes.defined[index] // If the question type not accepts multiquestion and there are multiple questions selcted store only the first if (newQuestionType && !MultiQuestionTypes.includes(newQuestionType) && questions.length > 1) { replace(questions[0]) } const nform: StepsFormValues = { ...form, - questionType: newQuestionType as VotingType, + questionType: newQuestionType, } setForm(nform) }} @@ -79,18 +73,11 @@ export const Questions = () => { setForm({ ...form, ...data }) next() } - return ( - - - + + + ) } diff --git a/src/components/ProcessCreate/StepForm/SaasFeatures.tsx b/src/components/ProcessCreate/StepForm/SaasFeatures.tsx index 7ff8b60a1..de24caa9b 100644 --- a/src/components/ProcessCreate/StepForm/SaasFeatures.tsx +++ b/src/components/ProcessCreate/StepForm/SaasFeatures.tsx @@ -1,7 +1,7 @@ import { Flex } from '@chakra-ui/react' import { FormProvider, SubmitHandler, useForm } from 'react-hook-form' -import { FeaturesKeys } from '~components/Account/useAccountPlan' -import { SaasFeatures as SaasFeaturesComponent } from '~components/ProcessCreate/Settings/SaasFeatures' +import { FeaturesKeys } from '~components/Pricing/Features' +import { Features as SaasFeaturesComponent } from '~components/ProcessCreate/Settings/SaasFeatures' import { ConfigurationValues } from '~components/ProcessCreate/StepForm/Info' import { StepsNavigation } from '~components/ProcessCreate/Steps/Navigation' import Wrapper from '~components/ProcessCreate/Steps/Wrapper' diff --git a/src/components/ProcessCreate/Steps/Confirm.tsx b/src/components/ProcessCreate/Steps/Confirm.tsx index db26f37ed..907e9745b 100644 --- a/src/components/ProcessCreate/Steps/Confirm.tsx +++ b/src/components/ProcessCreate/Steps/Confirm.tsx @@ -470,7 +470,9 @@ const electionFromForm = (form: StepsFormValues) => { ), startDate: form.electionType.autoStart ? undefined : new Date(form.startDate).getTime(), endDate: new Date(form.endDate).getTime(), - voteType: { maxVoteOverwrites: Number(form.maxVoteOverwrites) }, + voteType: { + maxVoteOverwrites: Number(form.maxVoteOverwrites), + }, temporarySecretIdentity: form.censusType === 'spreadsheet' && form.electionType.anonymous, meta: { generated: 'ui-scaffold', diff --git a/src/i18n/locales/ca.json b/src/i18n/locales/ca.json index d5024db65..892139f9f 100644 --- a/src/i18n/locales/ca.json +++ b/src/i18n/locales/ca.json @@ -5,8 +5,10 @@ "active": "Actiu", "all": "Tots", "already_member": "Ja ets membre?", - "anonymous": "anonymous", - "approval": "approval", + "anonymous": { + "description": "Voters will remain anonymous", + "title": "Anonymous" + }, "aside": { "has_already_voted": "El teu vot s'ha registrat correctament", "is_not_in_census": "No estàs en el cens", @@ -243,7 +245,6 @@ "selected_users": "Usuaris seleccionats:" } }, - "cumulative": "cumulative", "customization": "Customization", "delete_my_account": "Delete my account", "description": "Description", @@ -255,7 +256,6 @@ }, "email": "Correu electrònic", "email_placeholder": "your@email.com", - "emailReminder": "emailReminder", "error": { "error_doing_things": "Error al realizar la operación", "go_back": "Torna enrere", @@ -289,7 +289,17 @@ "title": "Faucet de Tokens Vocdoni", "tokens_you_own": "Tens {{ balance }} tokens." }, - "features": "Features", + "features": { + "approval": "Votació per aprovació", + "email_reminder": "Recordatoris per correu electrònic", + "memberships": "Fins a {{ value }} membresies", + "personalization": "Personalització", + "ranked": "Votació per rang", + "sms_notification": "Notificacions per SMS", + "sub_orgs": "Fins a {{ value }} suborganitzacions", + "title": "Característiques", + "weighted": "Votació ponderada" + }, "finished": "Finalitzat", "footer": { "company": "Empresa", @@ -730,7 +740,6 @@ "github": "Enllaç al Github de Vocdoni", "twitter": "Enllaç al Twitter de Vocdoni" }, - "liveStreaming": "liveStreaming", "loading": "Carregant...", "login": { "facebook": "Inicia sessió amb Facebook", @@ -769,7 +778,6 @@ "more_than_memberships_one": "més d'{{count}} membre", "more_than_memberships_many": "", "more_than_memberships_other": "més de {{count}} membres", - "multiple": "multiple", "name": "Name", "new_organization": { "create_organization_btn": "Crear Organització", @@ -807,7 +815,10 @@ }, "votings_list": "Llista de processos de votació" }, - "overwrite": "overwrite", + "overwrite": { + "description": "Voters will be able to overwrite their vote once", + "title": "Vote overwrite" + }, "password": "Contrasenya", "password_placeholder": "Min 8 characters", "password_reset": { @@ -815,17 +826,10 @@ "title": "Password reset" }, "passwords_do_not_match": "", - "personalization": "personalization", "plan_upgrade": { "dont_limit_yourself": "Don't limit yourself. Join the family of 725 orgs that trust <2>Vocdoni.", "feature_available_in": "Feature available in:", - "feature_unavailable": "Ups... el teu pla <2>{{ plan }} no suporta {{ feature }}.", - "plan": { - "basic": "Basic", - "custom": "Custom", - "free": "Free", - "pro": "Pro" - }, + "feature_unavailable": "Ups... el teu <2>pla {{ plan, lowercase }} no suporta {{ feature }}.", "satisfaction_rate": "97% satisfaction rate, for only 9€/month." }, "preview": "Preview", @@ -839,16 +843,6 @@ "dedicated_manager": "Dedicated account manager", "essential_subtitle": "Organitzacions petites o mitjanes o grups comunitaris amb necessitats bàsiques de votació.", "essential_title": "Essencial", - "features": { - "approval": "Votació per aprovació", - "email_reminder": "Recordatoris per correu electrònic", - "memberships": "Fins a {{ value }} membresies", - "personalization": "Personalització", - "ranked": "Votació per rang", - "sms_notification": "Notificacions per SMS", - "sub_orgs": "Fins a {{ value }} suborganitzacions", - "weighted": "Votació ponderada" - }, "free_subtitle": "Organitzacions petites o grups comunitaris amb necessitats bàsiques de votació.", "free_title": "Gratuït", "gpdr_compilance": "Compliment del GDPR", @@ -1041,7 +1035,6 @@ }, "recovery": "Recuperar compte" }, - "ranked": "ranked", "remove": "Remove", "reset_password_button": "Reset Password", "rights": "© 2024 Associació Vocdoni. Tots els drets reservats.", @@ -1049,7 +1042,10 @@ "read_permission": "Read-only access", "write_permission": "Can create and edit content" }, - "secretUntilTheEnd": "secretUntilTheEnd", + "secret_until_the_end": { + "description": "Vote contents will be encrypted till the end of the voting", + "title": "Secret until the end" + }, "settings": "Configuració", "share": { "copy": "Copia", @@ -1069,9 +1065,7 @@ "signup_last_name": "Cognom", "signup_subtitle": "Introdueix el teu correu electrònic i contrasenya per registrar-te!", "signup_title": "Registra't", - "single": "single", "skip": "Ometre", - "smsNotification": "smsNotification", "submit": "Submit", "subscription": "Subscripció", "support": "Suport", @@ -1145,7 +1139,5 @@ "voting_processes": "Processos de votació", "voting_questions": "Voting Questions", "voting_results": "Voting Results", - "website": "Website", - "weighted": "weighted", - "whiteLabel": "whiteLabel" + "website": "Website" } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e1a74dfae..dddb3c3a5 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -5,8 +5,10 @@ "active": "Active", "all": "All", "already_member": "Already a member?", - "anonymous": "anonymous", - "approval": "approval", + "anonymous": { + "description": "Voters will remain anonymous", + "title": "Anonymous" + }, "aside": { "has_already_voted": "Your vote is properly cast", "is_not_in_census": "You are not in the census", @@ -239,7 +241,6 @@ "selected_users": "Selected users:" } }, - "cumulative": "cumulative", "customization": "Customization", "delete_my_account": "Delete my account", "description": "Description", @@ -251,7 +252,6 @@ }, "email": "Email", "email_placeholder": "your@email.com", - "emailReminder": "emailReminder", "error": { "error_doing_things": "Error al realizar la operación", "go_back": "Go back", @@ -285,7 +285,17 @@ "title": "Vocdoni Tokens Faucet", "tokens_you_own": "You own {{ balance }} tokens." }, - "features": "Features", + "features": { + "approval": "Approval voting", + "email_reminder": "Email reminders", + "memberships": "Up to {{ count }} memberships", + "personalization": "Personalization", + "ranked": "Ranked voting", + "sms_notification": "SMS notifications", + "sub_orgs": "Up to {{ count }} sub-organizations", + "title": "Features", + "weighted": "Weighted voting" + }, "finished": "Finished", "footer": { "company": "Company", @@ -722,7 +732,6 @@ "github": "Link to Vocdoni's github", "twitter": "Link to Vocdoni's twitter" }, - "liveStreaming": "liveStreaming", "loading": "Loading...", "login": { "facebook": "Sign in with Facebook", @@ -760,7 +769,6 @@ }, "more_than_memberships_one": "more than {{count}} membership", "more_than_memberships_other": "more than {{count}} memberships", - "multiple": "multiple", "name": "Name", "new_organization": { "create_organization_btn": "Create Organization", @@ -798,7 +806,10 @@ }, "votings_list": "Votings Processes List" }, - "overwrite": "overwrite", + "overwrite": { + "description": "Voters will be able to overwrite their vote once", + "title": "Vote overwrite" + }, "password": "Password", "password_placeholder": "Min 8 characters", "password_reset": { @@ -806,17 +817,10 @@ "title": "Password reset" }, "passwords_do_not_match": "", - "personalization": "personalization", "plan_upgrade": { "dont_limit_yourself": "Don't limit yourself. Join the family of 725 orgs that trust <2>Vocdoni.", "feature_available_in": "Feature available in:", "feature_unavailable": "Oops ... your <2>free plan doesn't support {{feature}}.", - "plan": { - "basic": "Basic", - "custom": "Custom", - "free": "Free", - "pro": "Pro" - }, "satisfaction_rate": "97% satisfaction rate, for only 9€/month." }, "preview": "Preview", @@ -830,16 +834,6 @@ "dedicated_manager": "Dedicated account manager", "essential_subtitle": "Small or medium-sized orgs or community groups with basic voting needs.", "essential_title": "Essential", - "features": { - "approval": "Approval voting", - "email_reminder": "Email reminders", - "memberships": "Up to {{ count }} memberships", - "personalization": "Personalization", - "ranked": "Ranked voting", - "sms_notification": "SMS notifications", - "sub_orgs": "Up to {{ count }} sub-organizations", - "weighted": "Weighted voting" - }, "free_subtitle": "Small organizations or community groups with basic voting needs.", "free_title": "Free", "gpdr_compilance": "GDPR compliance", @@ -1028,7 +1022,6 @@ }, "recovery": "Account recovery" }, - "ranked": "ranked", "remove": "Remove", "reset_password_button": "Reset Password", "rights": "© 2024 Vocdoni Association. All Rights Reserved.", @@ -1036,7 +1029,10 @@ "read_permission": "Read-only access", "write_permission": "Can create and edit content" }, - "secretUntilTheEnd": "secretUntilTheEnd", + "secret_until_the_end": { + "description": "Vote contents will be encrypted till the end of the voting", + "title": "Secret until the end" + }, "settings": "Settings", "share": { "copy": "Copy", @@ -1056,9 +1052,7 @@ "signup_last_name": "Last Name", "signup_subtitle": "Enter your email and password to sign up!", "signup_title": "Sign Up", - "single": "single", "skip": "Skip", - "smsNotification": "smsNotification", "submit": "Submit", "subscription": "Subscription", "support": "Support", @@ -1132,7 +1126,5 @@ "voting_processes": "Voting Processes", "voting_questions": "Voting Questions", "voting_results": "Voting Results", - "website": "Website", - "weighted": "weighted", - "whiteLabel": "whiteLabel" + "website": "Website" } diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index a5a01ec08..331a23066 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -5,8 +5,10 @@ "active": "Activo", "all": "Todos", "already_member": "¿Ya eres miembro?", - "anonymous": "anonymous", - "approval": "approval", + "anonymous": { + "description": "Voters will remain anonymous", + "title": "Anonymous" + }, "aside": { "has_already_voted": "Tu voto ha sido registrado correctamente", "is_not_in_census": "No estás en el censo", @@ -243,7 +245,6 @@ "selected_users": "Usuarios seleccionados:" } }, - "cumulative": "cumulative", "customization": "Customization", "delete_my_account": "Delete my account", "description": "Description", @@ -255,7 +256,6 @@ }, "email": "Correo electrónico", "email_placeholder": "your@email.com", - "emailReminder": "emailReminder", "error": { "error_doing_things": "Error al realizar la operación", "go_back": "Volver atrás", @@ -289,7 +289,17 @@ "title": "Faucet de Tokens Vocdoni", "tokens_you_own": "Tienes {{ balance }} tokens." }, - "features": "Features", + "features": { + "approval": "Votación por aprobación", + "email_reminder": "Recordatorios por correo electrónico", + "memberships": "Hasta {{ value }} membresías", + "personalization": "Personalización", + "ranked": "Votación por rango", + "sms_notification": "Notificaciones por SMS", + "sub_orgs": "Hasta {{ value }} suborganizaciones", + "title": "Características", + "weighted": "Votación ponderada" + }, "finished": "Finalizado", "footer": { "company": "Empresa", @@ -730,7 +740,6 @@ "github": "Enlace al Github de Vocdoni", "twitter": "Enlace al Twitter de Vocdoni" }, - "liveStreaming": "liveStreaming", "loading": "Cargando...", "login": { "facebook": "Iniciar sesión usando Facebook", @@ -769,7 +778,6 @@ "more_than_memberships_one": "más de {{count}} membresía", "more_than_memberships_many": "", "more_than_memberships_other": "más de {{count}} membresías", - "multiple": "multiple", "name": "Nombre", "new_organization": { "create_organization_btn": "Crear Organización", @@ -807,7 +815,10 @@ }, "votings_list": "Lista de procesos de votación" }, - "overwrite": "overwrite", + "overwrite": { + "description": "Voters will be able to overwrite their vote once", + "title": "Vote overwrite" + }, "password": "Contraseña", "password_placeholder": "Min 8 characters", "password_reset": { @@ -815,17 +826,10 @@ "title": "Password reset" }, "passwords_do_not_match": "", - "personalization": "personalization", "plan_upgrade": { "dont_limit_yourself": "Don't limit yourself. Join the family of 725 orgs that trust <2>Vocdoni.", "feature_available_in": "Feature available in:", "feature_unavailable": "Oops ... your <2>free plan doesn't support {{feature}}.", - "plan": { - "basic": "Basic", - "custom": "Custom", - "free": "Free", - "pro": "Pro" - }, "satisfaction_rate": "97% satisfaction rate, for only 9€/month." }, "preview": "Preview", @@ -839,16 +843,6 @@ "dedicated_manager": "Dedicated account manager", "essential_subtitle": "Organizaciones pequeñas o medianas o grupos comunitarios con necesidades básicas de votación.", "essential_title": "Esencial", - "features": { - "approval": "Votación por aprobación", - "email_reminder": "Recordatorios por correo electrónico", - "memberships": "Hasta {{ value }} membresías", - "personalization": "Personalización", - "ranked": "Votación por rango", - "sms_notification": "Notificaciones por SMS", - "sub_orgs": "Hasta {{ value }} suborganizaciones", - "weighted": "Votación ponderada" - }, "free_subtitle": "Organizaciones pequeñas o grupos comunitarios con necesidades básicas de votación.", "free_title": "Gratis", "gpdr_compilance": "Cumplimiento del GDPR", @@ -1041,7 +1035,6 @@ }, "recovery": "Recuperar cuenta" }, - "ranked": "ranked", "remove": "Remove", "reset_password_button": "Reset Password", "rights": "© 2024 Asociación Vocdoni. Todos los derechos reservados.", @@ -1049,7 +1042,10 @@ "read_permission": "Read-only access", "write_permission": "Can create and edit content" }, - "secretUntilTheEnd": "secretUntilTheEnd", + "secret_until_the_end": { + "description": "Vote contents will be encrypted till the end of the voting", + "title": "Secret until the end" + }, "settings": "Configuración", "share": { "copy": "Copiar", @@ -1069,9 +1065,7 @@ "signup_last_name": "Apellido", "signup_subtitle": "¡Introduce tu correo electrónico y contraseña para registrarte!", "signup_title": "Regístrate", - "single": "single", "skip": "Omitir", - "smsNotification": "Notificación SMS", "submit": "Envía", "subscription": "Suscripción", "support": "Soporte", @@ -1145,7 +1139,5 @@ "voting_processes": "Procesos de votación", "voting_questions": "Voting Questions", "voting_results": "Voting Results", - "website": "Website", - "weighted": "weighted", - "whiteLabel": "whiteLabel" + "website": "Website" }