diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 4c5ed9e..49bcb47 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -41,7 +41,7 @@ <b-menu-item :to="{name: 'forms'}" tag="router-link" :active="activeItem.forms" - icon="newspaper-variant-outline" label="Forms"></b-menu-item> + icon="newspaper-variant-outline" :label="$t('menu.forms')"></b-menu-item> </b-menu-item><!-- lists --> <b-menu-item :expanded="activeGroup.subscribers" @@ -54,7 +54,7 @@ <b-menu-item :to="{name: 'import'}" tag="router-link" :active="activeItem.import" - icon="file-upload-outline" label="Import"></b-menu-item> + icon="file-upload-outline" :label="$t('menu.import')"></b-menu-item> </b-menu-item><!-- subscribers --> <b-menu-item :expanded="activeGroup.campaigns" diff --git a/frontend/src/components/Editor.vue b/frontend/src/components/Editor.vue index 809dacd..2863aaa 100644 --- a/frontend/src/components/Editor.vue +++ b/frontend/src/components/Editor.vue @@ -7,19 +7,19 @@ <div> <b-radio v-model="form.radioFormat" @input="onChangeFormat" :disabled="disabled" name="format" - native-value="richtext">Rich text</b-radio> + native-value="richtext">{{ $t('campaigns.richText') }}</b-radio> <b-radio v-model="form.radioFormat" @input="onChangeFormat" :disabled="disabled" name="format" - native-value="html">Raw HTML</b-radio> + native-value="html">{{ $t('campaigns.rawHTML') }}</b-radio> <b-radio v-model="form.radioFormat" @input="onChangeFormat" :disabled="disabled" name="format" - native-value="plain">Plain text</b-radio> + native-value="plain">{{ $t('campaigns.plainText') }}</b-radio> </div> </b-field> </div> <div class="column is-6 has-text-right"> <b-button @click="onTogglePreview" type="is-primary" - icon-left="file-find-outline">Preview</b-button> + icon-left="file-find-outline">{{ $t('campaigns.preview') }}</b-button> </div> </div> @@ -31,7 +31,7 @@ ref="quill" :options="options" :disabled="disabled" - placeholder="Content here" + :placeholder="$t('campaigns.contentHelp')" @change="onEditorChange($event)" @ready="onEditorReady($event)" /> @@ -142,7 +142,7 @@ export default { // Quill editor options. options: { - placeholder: 'Content here', + placeholder: this.$t('campaigns.contentHelp'), modules: { keyboard: { bindings: { @@ -188,7 +188,7 @@ export default { methods: { onChangeFormat(format) { this.$utils.confirm( - 'The content may lose some formatting. Are you sure?', + this.$t('campaigns.confirmSwitchFormat'), () => { this.form.format = format; this.onEditorChange(); diff --git a/frontend/src/views/Campaign.vue b/frontend/src/views/Campaign.vue index 7a5cf32..d911be3 100644 --- a/frontend/src/views/Campaign.vue +++ b/frontend/src/views/Campaign.vue @@ -65,15 +65,15 @@ :placeholder="$t('campaigns.sendToLists')" ></list-selector> - <b-field :label="$tc('terms.template')" label-position="on-border"> - <b-select :placeholder="$tc('terms.template')" v-model="form.templateId" + <b-field :label="$tc('globals.terms.template')" label-position="on-border"> + <b-select :placeholder="$tc('globals.terms.template')" v-model="form.templateId" :disabled="!canEdit" required> <option v-for="t in templates" :value="t.id" :key="t.id">{{ t.name }}</option> </b-select> </b-field> - <b-field :label="$tc('terms.messenger')" label-position="on-border"> - <b-select :placeholder="$tc('terms.messenger')" v-model="form.messenger" + <b-field :label="$tc('globals.terms.messenger')" label-position="on-border"> + <b-select :placeholder="$tc('globals.terms.messenger')" v-model="form.messenger" :disabled="!canEdit" required> <option v-for="m in serverConfig.messengers" :value="m" :key="m">{{ m }}</option> @@ -87,7 +87,7 @@ <hr /> <div class="columns"> - <div class="column is-2"> + <div class="column is-4"> <b-field :label="$t('campaigns.sendLater')"> <b-switch v-model="form.sendLater" :disabled="!canEdit" /> </b-field> @@ -99,7 +99,7 @@ <b-datetimepicker v-model="form.sendAtDate" :disabled="!canEdit" - :placeholder="$t('dateAndTime')" + :placeholder="$t('campaigns.dateAndTime')" icon="calendar-clock" :timepicker="{ hourFormat: '24' }" :datetime-formatter="formatDateTime" @@ -137,7 +137,7 @@ </section> </b-tab-item><!-- campaign --> - <b-tab-item label="Content" icon="text" :disabled="isNew"> + <b-tab-item :label="$t('campaigns.content')" icon="text" :disabled="isNew"> <section class="wrap"> <editor v-model="form.content" diff --git a/frontend/src/views/Campaigns.vue b/frontend/src/views/Campaigns.vue index bf06a92..f3b691b 100644 --- a/frontend/src/views/Campaigns.vue +++ b/frontend/src/views/Campaigns.vue @@ -28,12 +28,14 @@ :current-page="queryParams.page" :per-page="campaigns.perPage" :total="campaigns.total" hoverable backend-sorting @sort="onSort"> <template slot-scope="props"> - <b-table-column class="status" field="status" label="Status" + <b-table-column class="status" field="status" :label="$t('globals.fields.status')" width="10%" :id="props.row.id" sortable> <div> <p> <router-link :to="{ name: 'campaign', params: { 'id': props.row.id }}"> - <b-tag :class="props.row.status">{{ props.row.status }}</b-tag> + <b-tag :class="props.row.status"> + {{ $t(`campaigns.status.${props.row.status}`) }} + </b-tag> <span class="spinner is-tiny" v-if="isRunning(props.row.id)"> <b-loading :is-full-page="false" active /> </span> @@ -79,15 +81,15 @@ width="19%" sortable> <div class="fields timestamps" :set="stats = getCampaignStats(props.row)"> <p> - <label>Created</label> + <label>{{ $t('globals.fields.createdAt') }}</label> {{ $utils.niceDate(props.row.createdAt, true) }} </p> <p v-if="stats.startedAt"> - <label>Started</label> + <label>{{ $t('campaigns.startedAt') }}</label> {{ $utils.niceDate(stats.startedAt, true) }} </p> <p v-if="isDone(props.row)"> - <label>Ended</label> + <label>{{ $t('campaigns.ended') }}</label> {{ $utils.niceDate(stats.updatedAt, true) }} </p> <p v-if="stats.startedAt && stats.updatedAt" @@ -98,7 +100,8 @@ </div> </b-table-column> - <b-table-column field="stats" :class="props.row.status" label="Stats" width="18%"> + <b-table-column field="stats" :class="props.row.status" + :label="$t('campaigns.stats')" width="18%"> <div class="fields stats" :set="stats = getCampaignStats(props.row)"> <p> <label>{{ $t('campaigns.views') }}</label> diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 8fe22ed..7fd7511 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -14,13 +14,13 @@ <b-loading v-if="isCountsLoading" active :is-full-page="false" /> <article class="tile is-child notification"> <div class="columns is-mobile"> - <div class="column is-6"> + <div class="column is-4"> <p class="title">{{ $utils.niceNumber(counts.lists.total) }}</p> <p class="is-size-6 has-text-grey"> {{ $tc('globals.terms.list', counts.lists.total) }} </p> </div> - <div class="column is-6"> + <div class="column is-8"> <ul class="no is-size-7 has-text-grey"> <li> <label>{{ $utils.niceNumber(counts.lists.public) }}</label> @@ -45,13 +45,13 @@ <article class="tile is-child notification"> <div class="columns is-mobile"> - <div class="column is-6"> + <div class="column is-4"> <p class="title">{{ $utils.niceNumber(counts.campaigns.total) }}</p> <p class="is-size-6 has-text-grey"> {{ $tc('globals.terms.campaign', counts.campaigns.total) }} </p> </div> - <div class="column is-6"> + <div class="column is-8"> <ul class="no is-size-7 has-text-grey"> <li v-for="(num, status) in counts.campaigns.byStatus" :key="status"> <label>{{ num }}</label> {{ status }} @@ -66,14 +66,14 @@ <b-loading v-if="isCountsLoading" active :is-full-page="false" /> <article class="tile is-child notification"> <div class="columns is-mobile"> - <div class="column is-6"> + <div class="column is-4"> <p class="title">{{ $utils.niceNumber(counts.subscribers.total) }}</p> <p class="is-size-6 has-text-grey"> {{ $tc('globals.terms.subscriber', counts.subscribers.total) }} </p> </div> - <div class="column is-6"> + <div class="column is-8"> <ul class="no is-size-7 has-text-grey"> <li> <label>{{ $utils.niceNumber(counts.subscribers.blocklisted) }}</label> @@ -88,7 +88,7 @@ </div><!-- subscriber columns --> <hr /> <div class="columns"> - <div class="column is-6"> + <div class="column is-12"> <p class="title">{{ $utils.niceNumber(counts.messages) }}</p> <p class="is-size-6 has-text-grey"> {{ $t('dashboard.messagesSent') }} diff --git a/frontend/src/views/Import.vue b/frontend/src/views/Import.vue index b9c0d51..a213d94 100644 --- a/frontend/src/views/Import.vue +++ b/frontend/src/views/Import.vue @@ -8,7 +8,7 @@ <div> <div class="columns"> <div class="column"> - <b-field label="Mode"> + <b-field :label="$t('import.mode')"> <div> <b-radio v-model="form.mode" name="mode" native-value="subscribe">{{ $t('import.subscribe') }}</b-radio> diff --git a/frontend/src/views/Lists.vue b/frontend/src/views/Lists.vue index 618ba4e..08c5d34 100644 --- a/frontend/src/views/Lists.vue +++ b/frontend/src/views/Lists.vue @@ -50,9 +50,9 @@ </b-tag>{{ ' ' }} <a v-if="props.row.optin === 'double'" class="is-size-7 send-optin" href="#" @click="$utils.confirm(null, () => createOptinCampaign(props.row))"> - <b-tooltip label="Send opt-in campaign" type="is-dark"> + <b-tooltip :label="$t('lists.sendOptinCampaign')" type="is-dark"> <b-icon icon="rocket-launch-outline" size="is-small" /> - Send opt-in campaign + {{ $t('lists.sendOptinCampaign') }} </b-tooltip> </a> </div> diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index 7d27f0a..0232d75 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -161,14 +161,14 @@ </b-field> <b-field :label="$t('settings.privacy.allowWipe')" - message="$t('settings.privacy.allowWipeHelp')"> + :message="$t('settings.privacy.allowWipeHelp')"> <b-switch v-model="form['privacy.allow_wipe']" name="privacy.allow_wipe" /> </b-field> </div> </b-tab-item><!-- privacy --> - <b-tab-item label="Media uploads"> + <b-tab-item :label="$t('settings.media.title')"> <div class="items"> <b-field :label="$t('settings.media.provider')" label-position="on-border"> <b-select v-model="form['upload.provider']" name="upload.provider"> diff --git a/frontend/src/views/Subscribers.vue b/frontend/src/views/Subscribers.vue index 9784ef6..e9dca9c 100644 --- a/frontend/src/views/Subscribers.vue +++ b/frontend/src/views/Subscribers.vue @@ -106,11 +106,11 @@ hoverable checkable backend-sorting @sort="onSort"> <template slot="top-left"> <a href='' @click.prevent="exportSubscribers"> - <b-icon icon="cloud-download-outline" size="is-small" /> Export + <b-icon icon="cloud-download-outline" size="is-small" /> {{ $t('subscribers.export') }} </a> </template> <template slot-scope="props"> - <b-table-column field="status" label="Status" sortable> + <b-table-column field="status" :label="$t('globals.fields.status')" sortable> <a :href="`/subscribers/${props.row.id}`" @click.prevent="showEditForm(props.row)"> <b-tag :class="props.row.status"> @@ -119,7 +119,7 @@ </a> </b-table-column> - <b-table-column field="email" label="E-mail" sortable> + <b-table-column field="email" :label="$t('subscribers.email')" sortable> <a :href="`/subscribers/${props.row.id}`" @click.prevent="showEditForm(props.row)"> {{ props.row.email }} @@ -135,22 +135,22 @@ </b-taglist> </b-table-column> - <b-table-column field="name" label="Name" sortable> + <b-table-column field="name" :label="$t('globals.fields.name')" sortable> <a :href="`/subscribers/${props.row.id}`" @click.prevent="showEditForm(props.row)"> {{ props.row.name }} </a> </b-table-column> - <b-table-column field="lists" label="Lists" numeric centered> + <b-table-column field="lists" :label="$t('globals.terms.lists')" numeric centered> {{ listCount(props.row.lists) }} </b-table-column> - <b-table-column field="created_at" label="Created" sortable> + <b-table-column field="created_at" :label="$t('globals.fields.createdAt')" sortable> {{ $utils.niceDate(props.row.createdAt) }} </b-table-column> - <b-table-column field="updated_at" label="Updated" sortable> + <b-table-column field="updated_at" :label="$t('globals.fields.updatedAt')" sortable> {{ $utils.niceDate(props.row.updatedAt) }} </b-table-column> @@ -338,7 +338,7 @@ export default Vue.extend({ deleteSubscriber(sub) { this.$utils.confirm( - 'Are you sure?', + null, () => { this.$api.deleteSubscriber(sub.id).then(() => { this.querySubscribers(); diff --git a/i18n/en.json b/i18n/en.json index 1ba3411..934462c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6,9 +6,13 @@ "campaigns.clicks": "Clicks", "campaigns.confirmDelete": "Delete {name}", "campaigns.confirmSchedule": "This campaign will start automatically at the scheduled date and time.Schedule now?", + "campaigns.confirmSwitchFormat": "The content may lose formatting. Continue?", + "campaigns.content": "Content", + "campaigns.contentHelp": "Content here", "campaigns.continue": "Continue", "campaigns.copyOf": "Copy of {name}", "campaigns.dateAndTime": "Date and time", + "campaigns.ended": "Ended", "campaigns.errorSendTest": "Error sending test: {error}", "campaigns.fieldInvalidBody": "Error compiling campaign body: {error}", "campaigns.fieldInvalidFromEmail": "Invalid `from_email`.", @@ -33,9 +37,12 @@ "campaigns.onlyPausedDraft": "Only paused campaigns and drafts can be started.", "campaigns.onlyScheduledAsDraft": "Only scheduled campaigns can be saved as drafts.", "campaigns.pause": "Pause", + "campaigns.plainText": "Plain text", "campaigns.preview": "Preview", "campaigns.progress": "Progress", "campaigns.queryPlaceholder": "Name or subject", + "campaigns.rawHTML": "Raw HTML", + "campaigns.richText": "Rich text", "campaigns.schedule": "Schedule campaign", "campaigns.scheduled": "Scheduled", "campaigns.send": "Send", @@ -46,6 +53,14 @@ "campaigns.sent": "Sent", "campaigns.start": "Start campaign", "campaigns.started": "\"{name}\" started", + "campaigns.startedAt": "Started", + "campaigns.stats": "Stats", + "campaigns.status.cancelled": "Cancelled", + "campaigns.status.draft": "Draft", + "campaigns.status.finished": "Finished", + "campaigns.status.paused": "Paused", + "campaigns.status.running": "Running", + "campaigns.status.scheduled": "Scheduled", "campaigns.statusChanged": "\"{name}\" is {status}", "campaigns.subject": "Subject", "campaigns.testEmails": "E-mails", @@ -93,6 +108,13 @@ "globals.buttons.remove": "Remove", "globals.buttons.save": "Save", "globals.buttons.saveChanges": "Save changes", + "globals.day.1": "Mon", + "globals.day.2": "Tue", + "globals.day.3": "Wed", + "globals.day.4": "Thu", + "globals.day.5": "Fri", + "globals.day.6": "Sat", + "globals.day.7": "Sun", "globals.fields.createdAt": "Created", "globals.fields.id": "ID", "globals.fields.name": "Name", @@ -114,6 +136,18 @@ "globals.messages.notFound": "{name} not found", "globals.messages.passwordChange": "Enter a value to change", "globals.messages.updated": "\"{name}\" updated", + "globals.months.1": "Jan", + "globals.months.10": "Oct", + "globals.months.11": "Nov", + "globals.months.12": "Dec", + "globals.months.2": "Feb", + "globals.months.3": "Mar", + "globals.months.4": "Apr", + "globals.months.5": "May", + "globals.months.6": "Jun", + "globals.months.7": "Jul", + "globals.months.8": "Aug", + "globals.months.9": "Sep", "globals.terms.campaign": "Campaign | Campaigns", "globals.terms.campaigns": "Campaigns", "globals.terms.dashboard": "Dashboard", @@ -148,6 +182,7 @@ "import.invalidMode": "Invalid mode", "import.invalidParams": "Invalid params: {error}", "import.listSubHelp": "Lists to subscribe to.", + "import.mode": "Mode", "import.overwrite": "Overwrite?", "import.overwriteHelp": "Overwrite name and attribs of existing subscribers?", "import.recordsCount": "{num} / {total} records", @@ -165,6 +200,7 @@ "lists.optins.double": "Double opt-in", "lists.optins.single": "Single opt-in", "lists.sendCampaign": "Send campaign", + "lists.sendOptinCampaign": "Send opt-in campaign", "lists.type": "Type", "lists.typeHelp": "Public lists are open to the world to subscribe and their names may appear on public pages such as the subscription management page.", "lists.types.private": "Private", @@ -185,22 +221,12 @@ "menu.allSubscribers": "All subscribers", "menu.dashboard": "Dashboard", "menu.forms": "Forms", + "menu.import": "Import", "menu.logs": "Logs", "menu.media": "Media", "menu.newCampaign": "Create new", "menu.settings": "Settings", - "public.subNotFound": "Subscription not found.", "public.campaignNotFound": "The e-mail message was not found.", - "public.unsubTitle": "Unsubscribe", - "public.unsubHelp": "Do you want to unsubscribe from this mailing list?", - "public.unsubFull": "Also unsubscribe from all future e-mails.", - "public.unsub": "Unsubscribe", - "public.privacyTitle": "Privacy and data", - "public.privacyExport": "Export your data", - "public.privacyExportHelp": "A copy of your data will be e-mailed to you.", - "public.privacyWipe": "Wipe your data", - "public.privacyWipeHelp": "Delete all your subscriptions and related data from the database permanently.", - "public.privacyConfirmWipe": "Are you sure you want to delete all your subscription data permanently?", "public.confirmOptinSubTitle": "Confirm subscription", "public.confirmSub": "Confirm subscription", "public.confirmSubInfo": "You have been added to the following lists:", @@ -219,9 +245,20 @@ "public.noSubInfo": "There are no subscriptions to confirm", "public.noSubTitle": "No subscriptions", "public.notFoundTitle": "Not found", + "public.privacyConfirmWipe": "Are you sure you want to delete all your subscription data permanently?", + "public.privacyExport": "Export your data", + "public.privacyExportHelp": "A copy of your data will be e-mailed to you.", + "public.privacyTitle": "Privacy and data", + "public.privacyWipe": "Wipe your data", + "public.privacyWipeHelp": "Delete all your subscriptions and related data from the database permanently.", "public.subConfirmed": "Subscribed successfully", "public.subConfirmedTitle": "Confirmed", + "public.subNotFound": "Subscription not found.", "public.subPrivateList": "Private list", + "public.unsub": "Unsubscribe", + "public.unsubFull": "Also unsubscribe from all future e-mails.", + "public.unsubHelp": "Do you want to unsubscribe from this mailing list?", + "public.unsubTitle": "Unsubscribe", "public.unsubbedInfo": "You have unsubscribed successfully", "public.unsubbedTitle": "Unsubscribed", "public.unsubscribeTitle": "Unsubscribe from mailing list", @@ -282,14 +319,12 @@ "settings.performance.messageRate": "Message rate", "settings.performance.messageRateHelp": "Maximum number of messages to be sent out per second per worker in a second. If concurrency = 10 and message_rate = 10, then up to 10x10=100 messages may be pushed out every second. This, along with concurrency, should be tweaked to keep the net messages going out per second under the target message servers rate limits if any.", "settings.performance.name": "Performance", - "settings.performance.slidingWindow": "Enable sliding window limit", - "settings.performance.slidingWindowHelp": "Limit the total number of messages that are sent out in given period. On reaching this limit, messages are be held from sending until the time window clears.", "settings.performance.slidingWindowDuration": "Duration", "settings.performance.slidingWindowDurationHelp": "Duration of the sliding window period (m for minute, h for hour)", + "settings.performance.slidingWindowHelp": "Limit the total number of messages that are sent out in given period. On reaching this limit, messages are be held from sending until the time window clears.", "settings.performance.slidingWindowRate": "Max. messages", "settings.performance.slidingWindowRateHelp": "Maximum number of messages to send within the window duration", - "settings.privacy.allowBlocklist": "Allow blocklisting", "settings.privacy.allowBlocklistHelp": "Allow subscribers to unsubscribe from all mailing lists and mark themselves as blocklisted?", "settings.privacy.allowExport": "Allow exporting", @@ -335,8 +370,8 @@ "subscribers.attribsHelp": "Attributes are defined as a JSON map, for example:", "subscribers.blocklistedHelp": "Blocklisted subscribers will never receive any e-mails.", "subscribers.confirmBlocklist": "Blocklist {num} subscriber(s)?", - "subscribers.confirmExport": "Export {num} subscriber(s)?", "subscribers.confirmDelete": "Delete {num} subscriber(s)?", + "subscribers.confirmExport": "Export {num} subscriber(s)?", "subscribers.downloadData": "Download data", "subscribers.email": "E-mail", "subscribers.emailExists": "E-mail already exists", @@ -346,6 +381,7 @@ "subscribers.errorNoListsGiven": "No lists given", "subscribers.errorPreparingQuery": "Error preparing subscriber query: {error}", "subscribers.errorSendingOptin": "Error sending opt-in e-mail", + "subscribers.export": "Export", "subscribers.invalidAction": "Invalid action", "subscribers.invalidEmail": "Invalid email", "subscribers.invalidJSON": "Invalid JSON in attributes", @@ -381,4 +417,4 @@ "templates.placeholderHelp": "The placeholder {placeholder} should appear exactly once in the template.", "templates.preview": "Preview", "templates.rawHTML": "Raw HTML" -} +} \ No newline at end of file