Text messages Recommended
Keep each text to one segment
Keep each text within one segment, and test every template for its encoding and length.
How to do it
A text in the GSM-7 alphabet fits 160 characters, or 153 per part when split. One character outside it, such as an emoji or a curly quote, switches the whole text to UCS-2. Then it fits 70 characters, or 67 per part, and an emoji counts as two. GSM-7 extension characters, such as € and the square brackets, count as two. Each part is billed as one message. Replace curly quotes and long dashes with plain ones, and put long details behind a link.
Example
const GSM7 = /^[@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !"#¤%&'()*+,\-./0-9:;<=>?¡A-ZÄÖÑܧ¿a-zäöñüà^{}\\[~\]|€]*$/;
const GSM7_EXTENSION = /[\^{}\\[~\]|€]/g;
export function segments(text) {
if (!GSM7.test(text)) return text.length <= 70 ? 1 : Math.ceil(text.length / 67);
const units = text.length + (text.match(GSM7_EXTENSION)?.length ?? 0);
return units <= 160 ? 1 : Math.ceil(units / 153);
}
import math
import re
GSM7 = re.compile(r"^[@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,\-./0-9:;<=>?¡A-ZÄÖÑܧ¿a-zäöñüà^{}\\\[~\]|€]*$")
GSM7_EXTENSION = re.compile(r"[\^{}\\\[~\]|€]")
def segments(text: str) -> int:
if not GSM7.fullmatch(text):
utf16_units = len(text.encode("utf-16-le")) // 2
return 1 if utf16_units <= 70 else math.ceil(utf16_units / 67)
units = len(text) + len(GSM7_EXTENSION.findall(text))
return 1 if units <= 160 else math.ceil(units / 153)
How to check
A unit test renders every template with the longest sample data and counts segments. Each one is 1, or 2 where the catalogue allows it.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers notifications playbook: https://agentwranglers.se/playbook/notifications/text-messages/sms-segments.