Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
00dbc1a
fix(email-campaigns): use p_user_id arg in get_contacts_table RPC
malek10xdev Jul 3, 2026
37eedf7
fix(supabase): re-apply missing sms_campaigns columns
malek10xdev Jul 3, 2026
a60fadd
feat(sms-campaigns): auto-discover simple-sms-gateway API spec
malek10xdev Jul 3, 2026
85faf45
feat(sms-fleet): reject unreachable gateways + add re-detect route
malek10xdev Jul 3, 2026
dbd8232
fix(sms-campaigns): accept national-format phone numbers
malek10xdev Jul 3, 2026
5214aa1
feat(frontend): add auto-detect and re-detect to SMS gateway UI
malek10xdev Jul 3, 2026
286af95
fix(sms-campaigns): honor ?dryRun=true on POST /fleet/gateways
malek10xdev Jul 8, 2026
0bb8f5a
refactor(sms-fleet): replace auto-discover with curated SMS app registry
malek10xdev Jul 8, 2026
c065917
fix(sms-fleet): rename iOS provider, remove appId, fix processor
malek10xdev Jul 9, 2026
2f13313
refactor(sms-fleet): dedup gateway probe, tighten types, rename migra…
malek10xdev Jul 9, 2026
6c2eead
fix(sms-campaigns): surface errors and break stuck-processing loop
malek10xdev Jul 9, 2026
6f589e8
chore(sms-campaigns): add per-step debug logging in processor
malek10xdev Jul 9, 2026
01ea0e7
fix(sms-campaigns): read project_url from vault in cron trigger
malek10xdev Jul 9, 2026
852eeff
fix(logger): surface undefined fields as null in structured logs
malek10xdev Jul 9, 2026
d737375
revert: drop wall-clock, audit, debug-log, vault-cron, and logger cha…
malek10xdev Jul 9, 2026
7394db9
chore(sms-campaigns): add console.log debug statements for hang diagn…
malek10xdev Jul 9, 2026
d86f69a
fix(config): set edge_runtime policy to per_worker for background tasks
malek10xdev Jul 9, 2026
9e9fca2
chore(sms-campaigns): add recipient-loop diagnostic logs
malek10xdev Jul 9, 2026
8da04cb
fix(sms-campaigns): assign recipients outside try block to fix variab…
malek10xdev Jul 9, 2026
64ced39
chore(sms-campaigns): log gatewayIds and first assignment before fetch
malek10xdev Jul 9, 2026
bcb5803
fix(sms-campaigns): fix assignments shadowing and 'completed' status
malek10xdev Jul 10, 2026
6351531
chore(sms-campaigns): remove diagnostic console.log statements
malek10xdev Jul 10, 2026
1826b5e
fix(sms-fleet): defer delete-gateway call with nextTick to fix dialog…
malek10xdev Jul 10, 2026
7742d15
fix(sms-fleet): move ConfirmDialog to global app.vue
malek10xdev Jul 10, 2026
4c53fea
fix(sms-campaigns): resolve DeepSource issues and replace hardcoded s…
malek10xdev Jul 10, 2026
67bbbb9
fix: register all edge functions in config.toml with correct verify_jwt
malek10xdev Jul 10, 2026
376c859
fix(sms-campaigns): resolve remaining DeepSource minor issues
malek10xdev Jul 10, 2026
9218971
fix(sms-campaigns): use correct skipcq syntax for complexity skip
malek10xdev Jul 10, 2026
da18816
refactor(providers): drop what-comments in sms-gateway-provider
malek10xdev Jul 10, 2026
4edbe97
refactor(sms-fleet): drop redundant probe/handler comments
malek10xdev Jul 10, 2026
e377274
refactor(sms-campaigns-process): extract createProviderForGateway
malek10xdev Jul 10, 2026
3ca4880
fix(config): comment passive-mining import_map (no deno.json exists)
malek10xdev Jul 10, 2026
001f22d
fix(config): disable verify_jwt for mining-sources OAuth callback
malek10xdev Jul 10, 2026
e2815d7
fix(config): disable verify_jwt for email-campaigns
malek10xdev Jul 10, 2026
aefe01d
fix: address DeepSource anti-pattern issues
malek10xdev Jul 10, 2026
d744aec
Merge branch 'main' into fix/qa-bugs-batch
malek10xdev Jul 10, 2026
87df808
fix(google-contacts): use canonicalForm for phone numbers
malek10xdev Jul 10, 2026
bb08e31
fix(contacts): resolve org name on correct object in realtime handler
malek10xdev Jul 10, 2026
364787d
perf: skip org lookup during mining + hide works_for column
malek10xdev Jul 10, 2026
21432e3
fix: chunk .in() query to prevent URI-too-long during bulk enrichment
malek10xdev Jul 10, 2026
5a6a51b
style: prettier fix frontend formatting
malek10xdev Jul 10, 2026
2a8b151
fix: add eslint-disable for intentional no-await-in-loop in enrichmen…
malek10xdev Jul 10, 2026
b0909ab
refactor(sms): drop ios sms-gateway provider from edge functions
malek10xdev Jul 13, 2026
2e76dea
feat(sms): drop sms-gateway from sms_fleet_gateways provider check
malek10xdev Jul 13, 2026
482887b
refactor(sms-ui): drop ios sms-gateway, surface {{name}} syntax, show…
malek10xdev Jul 13, 2026
61b5128
fix(sms): make click tracker failures non-fatal, log partial-failures…
malek10xdev Jul 13, 2026
4e722c9
Merge branch 'main' into fix/qa-bugs-batch
malek10xdev Jul 13, 2026
d68aa8c
fix(sms-ui): re-apply iOS removal after PR #2860 merge
malek10xdev Jul 13, 2026
83db78c
ci(deepsource): resolve critical and major issues in PR #2862
malek10xdev Jul 13, 2026
f7b9779
ci(frontend): fix prettier formatting in src/types/sms-fleet.ts
malek10xdev Jul 13, 2026
15d023c
fix(sms): migrate existing ios gateways in check constraint update; h…
malek10xdev Jul 21, 2026
c7a1608
feat(sms-ui): surface partial failures in campaign list and validate …
malek10xdev Jul 21, 2026
906d844
style: prettier fix SmsCampaignComposerDialog and campaigns page
malek10xdev Jul 21, 2026
1462902
ci(deepsource): add skipcq for pre-existing cyclomatic complexity in …
malek10xdev Jul 21, 2026
f5dc45b
feat(sms): add mock gateway server and integration tests for campaign…
malek10xdev Jul 21, 2026
de310ed
refactor(sms): extract testable processCampaignRecipients; fix mock s…
malek10xdev Jul 21, 2026
bb8c2cf
docs(sms-mock): update docs with correct edge function URL and fronte…
malek10xdev Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions frontend/src/components/campaigns/SmsCampaignComposerDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,17 @@
:placeholder="t('message_placeholder')"
@input="updateCharCount"
/>
<small class="text-surface-500">{{ t('attribute_syntax_help') }}</small>
<div class="flex justify-between text-xs text-surface-500">
<span>{{ smsParts }} {{ smsParts === 1 ? 'SMS' : 'SMS' }}</span>
<span>{{ charactersLeftLabel }}</span>
</div>
<small v-if="showFieldError('messageTemplate')" class="text-red-500">
{{ validationErrors.messageTemplate }}
</small>
<small v-if="validationErrors.placeholders" class="text-red-500">
{{ validationErrors.placeholders }}
</small>
</div>

<div class="border border-surface-200 rounded-md p-3">
Expand Down Expand Up @@ -271,11 +275,12 @@ const form = reactive({
selectedGatewayIds: [] as string[],
});

type FormField = 'messageTemplate' | 'gateways';
type FormField = 'messageTemplate' | 'gateways' | 'placeholders';

const touched = reactive<Record<FormField, boolean>>({
messageTemplate: false,
gateways: false,
placeholders: false,
});

const charCount = ref(0);
Expand Down Expand Up @@ -359,20 +364,32 @@ const toggleAttributeMenu = (event: MouseEvent) => {
};

const validationErrors = computed<Record<FormField, string>>(() => {
return {
const errors: Record<FormField, string> = {
messageTemplate: form.messageTemplate.trim().length
? ''
: t('message_required'),
gateways: '',
placeholders: '',
};

if (form.messageTemplate.trim().length > 0) {
const singleBraceRegex =
/(?<![{])\{(?:name|email|worksFor|jobTitle|location)\}(?![}])/;
if (singleBraceRegex.test(form.messageTemplate)) {
errors.placeholders = t('placeholder_syntax_error');
}
}

return errors;
});

const showFieldError = (field: FormField) => {
return touched[field] && validationErrors.value[field];
};

const isFormValid = computed(() =>
Object.values(validationErrors.value).every((value) => !value),
const isFormValid = computed(
() =>
!validationErrors.value.messageTemplate && !validationErrors.value.gateways,
);

const selectedContactsLength = computed(() => {
Expand Down Expand Up @@ -658,18 +675,20 @@ watch(() => form.footerTextTemplate, updateCharCount);
"sms_limit_note": "SMS is distributed across your gateways. Each gateway has a default limit of 200 SMS/day and 200 recipients/month.",
"sms_gateways": "SMS Gateways",
"message": "Message",
"message_placeholder": "Enter your SMS message here...",
"message_placeholder": "Hi {{name}}, your message here... Use {{name}} for the recipient's name.",
"message_required": "Message is required",
"characters_left": "{count} characters left",
"insert_person_attribute_body": "Insert contact attribute in SMS",
"attribute_syntax_help": "Insert placeholders like {{name}}, {{email}}, or {{worksFor}} — they will be replaced with the contact's data.",
"placeholder_syntax_error": "Use {{name}} (double braces) instead of {name} for placeholders",
"show_advanced": "Show advanced options",
"hide_advanced": "Hide advanced options",
"monthly_recipient_limit": "Monthly recipient limit",
"monthly_recipient_limit_help": "Maximum number of recipients per month (max 200)",
"monthly_recipient_limit_exceeded": "You selected {selected} recipients, but the limit is {limit}.",
"footer_template": "Footer template",
"footer_template_help": "Editable footer appended to each SMS.",
"footer_template_hint": "Use {placeholder} to insert the unsubscribe link.",
"footer_template_hint": "Use {{unsubscribeUrl}} to insert the unsubscribe link.",
"default_footer_template": "Unsubscribe me: {placeholder}",
"use_short_links": "Use short links",
"use_short_links_help": "Shorten URLs to reduce message length. Falls back to full URL if shortening fails.",
Expand Down Expand Up @@ -698,18 +717,20 @@ watch(() => form.footerTextTemplate, updateCharCount);
"sms_limit_note": "Les SMS sont distribués sur vos passerelles. Chaque passerelle a une limite par défaut de 200 SMS/jour et 200 destinataires/mois.",
"sms_gateways": "Passerelles SMS",
"message": "Message",
"message_placeholder": "Entrez votre message SMS ici...",
"message_placeholder": "Bonjour {{name}}, votre message ici... Utilisez {{name}} pour le nom du destinataire.",
"message_required": "Le message est requis",
"characters_left": "{count} caractères restants",
"insert_person_attribute_body": "Insérer un attribut contact dans le SMS",
"attribute_syntax_help": "Insérez des variables comme {{name}}, {{email}} ou {{worksFor}} — elles seront remplacées par les données du contact.",
"placeholder_syntax_error": "Utilisez {{nom}} (doubles accolades) au lieu de {nom} pour les variables",
"show_advanced": "Afficher les options avancées",
"hide_advanced": "Masquer les options avancées",
"monthly_recipient_limit": "Limite mensuelle de destinataires",
"monthly_recipient_limit_help": "Nombre maximum de destinataires par mois (max 200)",
"monthly_recipient_limit_exceeded": "Vous avez sélectionné {selected} destinataires, mais la limite est de {limit}.",
"footer_template": "Modèle de pied de message",
"footer_template_help": "Pied de message modifiable ajouté à chaque SMS.",
"footer_template_hint": "Utilisez {placeholder} pour insérer le lien de désinscription.",
"footer_template_hint": "Utilisez {{unsubscribeUrl}} pour insérer le lien de désinscription.",
"default_footer_template": "Se désinscrire : {placeholder}",
"use_short_links": "Utiliser des liens courts",
"use_short_links_help": "Raccourcit les URLs pour réduire la longueur du message. Revient à l'URL complète en cas d'échec.",
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/components/mining/table/MiningTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@
<MultiSelect
v-model="$contactsStore.visibleColumns"
:options="visibleColumnsOptions"
:option-disabled="disabledColumns"
option-label="label"
class="min-w-56"
fluid
Expand All @@ -272,6 +273,7 @@
t('visible_columns', $contactsStore.visibleColumns.length)
"
:max-selected-labels="0"
@change="onSelectColumnsChange"
/>
</li>
</ul>
Expand Down Expand Up @@ -1374,6 +1376,16 @@
{ label: t('mining_id'), value: 'mining_id' },
]);

function disabledColumns(column: { label: string; value: string }) {
return column.value === 'contacts';
}
function onSelectColumnsChange() {
// PrimeVue bug fix: MultiSelect: Can deselect disabled options https://github.com/primefaces/primevue/issues/5490
if (!visibleColumns.value.includes('contacts')) {
visibleColumns.value.push('contacts');
}
}

function getDefaultVisibleColumns() {
return ['contacts', 'name', 'location', 'works_for', 'job_title'];
}
Expand Down Expand Up @@ -1478,7 +1490,7 @@
await $contactsStore.reloadContacts();

if (!$contactsStore.contactCount && (await $contactsStore.hasPersons())) {
console.log(

Check warning on line 1493 in frontend/src/components/mining/table/MiningTable.vue

View workflow job for this annotation

GitHub Actions / frontend

Unexpected console statement
'Data in persons table but not in refinedpersons, refining contacts...',
);
await $contactsStore.refineContacts();
Expand Down
50 changes: 6 additions & 44 deletions frontend/src/components/sms-fleet/FleetGatewaySelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,6 @@
outlined
@click="setupSelectedProvider = 'simple-sms-gateway'"
/>
<Button
:label="t('ios_sms_gateway')"
outlined
@click="setupSelectedProvider = 'sms-gateway'"
/>
</div>
</div>
<div v-else class="flex flex-col gap-2">
Expand Down Expand Up @@ -164,22 +159,6 @@
{{ t('open_playstore') }}
</a>
</template>
<template v-else-if="setupSelectedProvider === 'sms-gateway'">
<p class="m-0">{{ t('ios_sms_gateway_setup_intro') }}</p>
<ol class="pl-4 m-0 list-decimal">
<li class="mb-2">{{ t('ios_sms_gateway_setup_step_1') }}</li>
<li class="mb-2">{{ t('ios_sms_gateway_setup_step_2') }}</li>
<li>{{ t('ios_sms_gateway_setup_step_3') }}</li>
</ol>
<a
:href="iosSmsGatewayDownloadUrl"
target="_blank"
rel="noopener noreferrer"
class="underline"
>
{{ t('ios_app_store') }}
</a>
</template>
</div>
</div>
</Dialog>
Expand Down Expand Up @@ -308,15 +287,15 @@ const $smsFleetStore = useSmsFleetStore();

const showAddDialog = ref(false);
const showSetupDialog = ref(false);
const setupSelectedProvider = ref<
'smsgate' | 'simple-sms-gateway' | 'sms-gateway' | null
>(null);
const setupSelectedProvider = ref<'smsgate' | 'simple-sms-gateway' | null>(
null,
);
const isSubmitting = ref(false);

const gatewayName = ref('');
const selectedProvider = ref<
'smsgate' | 'simple-sms-gateway' | 'sms-gateway' | null
>('simple-sms-gateway');
const selectedProvider = ref<'smsgate' | 'simple-sms-gateway' | null>(
'simple-sms-gateway',
);
const dailyLimit = ref(200);
const monthlyLimit = ref(200);
const providerFormRef = ref<InstanceType<typeof ProviderForm> | null>(null);
Expand Down Expand Up @@ -363,7 +342,6 @@ const toggleGateway = (gatewayId: string) => {
const providerOptions = [
{ label: 'SMSGate', value: 'smsgate' },
{ label: 'Simple SMS Gateway', value: 'simple-sms-gateway' },
{ label: 'SMS Gateway (iOS)', value: 'sms-gateway' },
];

function handleFormSubmit(config: {
Expand Down Expand Up @@ -425,14 +403,11 @@ function openSetupDialog() {
const smsgateDownloadUrl = 'https://sms-gate.app/';
const simpleSmsGatewayDownloadUrl =
'https://play.google.com/store/apps/details?id=com.pabrikaplikasi.simplesmsgateway';
const iosSmsGatewayDownloadUrl =
'https://apps.apple.com/us/app/sms-gateway/id6767250233';

const getProviderIcon = (provider: SmsGatewayProvider) => {
const icons: Record<SmsGatewayProvider, string> = {
smsgate: 'pi pi-android',
'simple-sms-gateway': 'pi pi-mobile',
'sms-gateway': 'pi pi-mobile',
twilio: 'pi pi-phone',
};
return icons[provider] || 'pi pi-mobile';
Expand All @@ -442,7 +417,6 @@ const formatProvider = (provider: SmsGatewayProvider) => {
const names: Record<SmsGatewayProvider, string> = {
smsgate: 'SMSGate',
'simple-sms-gateway': 'Simple SMS Gateway',
'sms-gateway': 'SMS Gateway (iOS)',
twilio: 'Twilio',
};
return names[provider] || provider;
Expand Down Expand Up @@ -481,12 +455,6 @@ const formatProvider = (provider: SmsGatewayProvider) => {
"simple_sms_gateway_setup_step_2": "Configure the server URL in the app settings",
"simple_sms_gateway_setup_step_3": "Enter the server URL in the form above",
"open_playstore": "Open in Play Store",
"ios_sms_gateway": "SMS Gateway (iOS)",
"ios_sms_gateway_setup_intro": "To use SMS Gateway (iOS) as your SMS gateway, install the iOS app and configure the server URL.",
"ios_sms_gateway_setup_step_1": "Install SMS Gateway from the App Store on your iOS device",
"ios_sms_gateway_setup_step_2": "Open the app and configure the server URL in its settings",
"ios_sms_gateway_setup_step_3": "Enter the server URL (e.g. http://192.168.x.x:8080) in the form below",
"ios_app_store": "Open App Store",
"how_to_install": "How to install",
"select_provider_to_see_instructions": "Select a provider to see installation instructions",
"smsgate": "SMSGate",
Expand Down Expand Up @@ -524,12 +492,6 @@ const formatProvider = (provider: SmsGatewayProvider) => {
"simple_sms_gateway_setup_step_2": "Configurez l'URL du serveur dans les paramètres de l'application",
"simple_sms_gateway_setup_step_3": "Entrez l'URL du serveur dans le formulaire ci-dessus",
"open_playstore": "Ouvrir sur Play Store",
"ios_sms_gateway": "SMS Gateway (iOS)",
"ios_sms_gateway_setup_intro": "Pour utiliser SMS Gateway (iOS) comme passerelle SMS, installez l'app iOS et configurez l'URL du serveur.",
"ios_sms_gateway_setup_step_1": "Installez SMS Gateway depuis l'App Store sur votre appareil iOS",
"ios_sms_gateway_setup_step_2": "Ouvrez l'app et configurez l'URL du serveur dans ses paramètres",
"ios_sms_gateway_setup_step_3": "Entrez l'URL du serveur (ex. http://192.168.x.x:8080) dans le formulaire ci-dessous",
"ios_app_store": "Ouvrir l'App Store",
"how_to_install": "Comment installer",
"select_provider_to_see_instructions": "Sélectionnez un fournisseur pour voir les instructions d'installation",
"smsgate": "SMSGate",
Expand Down
20 changes: 1 addition & 19 deletions frontend/src/components/sms-fleet/ProviderForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { z } from 'zod';

const { t } = useI18n({ useScope: 'local' });

type SupportedProvider = 'smsgate' | 'simple-sms-gateway' | 'sms-gateway';
type SupportedProvider = 'smsgate' | 'simple-sms-gateway';

const props = defineProps<{
provider: SupportedProvider;
Expand Down Expand Up @@ -45,9 +45,6 @@ const isValid = computed(() => {
baseUrl: baseUrl.value,
}).success;
}
// Both Android Simple SMS Gateway and iOS SMS Gateway share the same
// URL form. Dispatch between them is driven by `props.provider`
// ("simple-sms-gateway" vs "sms-gateway") on the backend.
return selfHostedGatewaySchema.safeParse({
baseUrl: baseUrl.value,
}).success;
Expand Down Expand Up @@ -77,21 +74,6 @@ function handleSubmit() {
return;
}

if (props.provider === 'sms-gateway') {
// The iOS app exposes `POST /send-sms` with the same URL contract as
// the Android Simple SMS Gateway. We re-use the
// `simpleSmsGatewayBaseUrl` storage key; the `provider` field
// ("sms-gateway") tells the backend to dispatch to the
// `SmsGatewayProvider`.
emit('submit', {
provider: 'sms-gateway',
config: {
simpleSmsGatewayBaseUrl: baseUrl.value,
},
});
return;
}

emit('submit', {
provider: 'simple-sms-gateway',
config: {
Expand Down
23 changes: 16 additions & 7 deletions frontend/src/components/sms-fleet/SmsFleetManagement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import Checkbox from 'primevue/checkbox';
import ProgressSpinner from 'primevue/progressspinner';
import type { SmsGatewayProvider, SmsFleetGateway } from '@/types/sms-fleet';

type SupportedProvider = 'smsgate' | 'simple-sms-gateway' | 'sms-gateway';
type SupportedProvider = 'smsgate' | 'simple-sms-gateway';

const props = defineProps<{
autoAdd?: boolean;
Expand Down Expand Up @@ -63,7 +63,6 @@ const providerOptions = computed(() => [
label: t('simple_sms_gateway_option'),
value: 'simple-sms-gateway' as const,
},
{ label: t('ios_sms_gateway_option'), value: 'sms-gateway' as const },
]);

function handleFormValid(valid: boolean) {
Expand Down Expand Up @@ -132,7 +131,6 @@ function getDefaultName(provider: SupportedProvider): string {
const names: Record<SupportedProvider, string> = {
smsgate: 'SMSGate Gateway',
'simple-sms-gateway': 'SMS Gateway',
'sms-gateway': 'SMS Gateway (iOS)',
};
return names[provider];
}
Expand Down Expand Up @@ -213,7 +211,6 @@ function getProviderLabel(provider: SmsGatewayProvider): string {
const labels: Record<SmsGatewayProvider, string> = {
smsgate: 'SMSGate',
'simple-sms-gateway': 'Simple SMS Gateway',
'sms-gateway': 'SMS Gateway (iOS)',
twilio: 'Twilio',
};
return labels[provider] || provider;
Expand All @@ -223,7 +220,6 @@ function getProviderIcon(provider: SmsGatewayProvider): string {
const icons: Record<SmsGatewayProvider, string> = {
smsgate: 'pi pi-android',
'simple-sms-gateway': 'pi pi-mobile',
'sms-gateway': 'pi pi-mobile',
twilio: 'pi pi-phone',
};
return icons[provider] || 'pi pi-mobile';
Expand Down Expand Up @@ -295,6 +291,17 @@ onMounted(() => {
:label="$screenStore.size.md ? t('delete') : undefined"
@click="confirmDelete(gateway)"
/>
<span v-if="gateway.daily_limit > 0" class="text-xs text-surface-500">
{{
t('daily_usage_with_limit', {
sent: gateway.sent_today,
limit: gateway.daily_limit,
})
}}
</span>
<span v-else class="text-xs text-surface-500">
{{ t('daily_usage_unlimited', { sent: gateway.sent_today }) }}
</span>
<Tag
:value="gateway.is_active ? t('active') : t('inactive')"
:severity="gateway.is_active ? 'success' : 'secondary'"
Expand Down Expand Up @@ -447,7 +454,8 @@ onMounted(() => {
"delete_confirm_header": "Confirm Deletion",
"save_changes": "Save Changes",
"simple_sms_gateway_option": "Simple SMS Gateway",
"ios_sms_gateway_option": "SMS Gateway (iOS)"
"daily_usage_with_limit": "{sent}/{limit} today",
"daily_usage_unlimited": "{sent} sent today"
},
"fr": {
"sms_fleet_management": "Gestion de la flotte SMS",
Expand Down Expand Up @@ -478,7 +486,8 @@ onMounted(() => {
"delete_confirm_header": "Confirmer la suppression",
"save_changes": "Enregistrer les modifications",
"simple_sms_gateway_option": "Simple SMS Gateway",
"ios_sms_gateway_option": "SMS Gateway (iOS)"
"daily_usage_with_limit": "{sent}/{limit} aujourd'hui",
"daily_usage_unlimited": "{sent} envoyés aujourd'hui"
}
}
</i18n>
Loading
Loading