---
title: "Count and cut text by visible characters · Agent Wranglers"
url: https://agentwranglers.se/playbook/localization/text-handling/grapheme-clusters
id: LOC-50
level: Recommended
updated: 2026-10-22
build: a2a6bdfed830
tokens: 799
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Count and cut text by visible characters

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Text handling](https://agentwranglers.se/playbook/localization/text-handling.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-22.

- ID: LOC-50
- Level: Recommended
- Chapter: [Text handling](https://agentwranglers.se/playbook/localization/text-handling.md)
- Rule: Count characters for length limits, and cut previews, by extended grapheme clusters. Keep a byte limit for storage beside it.
- Check: A unit test cuts `👩‍👩‍👧 home` to 1 character and gets `👩‍👩‍👧`. The form counter shows 1 for `e` followed by a combining acute accent.
- How: One visible character can hold many code points, such as an emoji family or a letter with a combining accent. Cutting by code units or code points splits it and leaves broken symbols. Use `Intl.Segmenter` in JavaScript, `\X` in Python's `regex` module or `BreakIterator` in Java. UAX #29 calls a grapheme count a user-perceived limit and pairs it with a data size limit. Normalise the text first, as [Sort and compare text by locale](https://agentwranglers.se/playbook/localization/text-handling/locale-collation.md) says.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### JavaScript example

```js
const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
export const truncate = (text, maxCharacters) =>
  Array.from(graphemes.segment(text), part => part.segment).slice(0, maxCharacters).join('');
```

### Python example

```python
import regex

def truncate(text: str, max_characters: int) -> str:
    return "".join(regex.findall(r"\X", text)[:max_characters])
```

## Sources

- [Unicode UAX #29 (Unicode Text Segmentation)](https://www.unicode.org/reports/tr29/)
- [ECMA-402 (Intl.Segmenter)](https://tc39.es/ecma402/)

## Related rules

- LOC-15 [Sort and compare text by locale](https://agentwranglers.se/playbook/localization/text-handling/locale-collation.md) in Localization
- LOC-56 [Cover every script with fonts and break words with a segmenter](https://agentwranglers.se/playbook/localization/layout-and-scripts/scripts-fonts-and-line-breaks.md) in Localization

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/localization/text-handling/grapheme-clusters">Agent Wranglers localization playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Accept names and addresses from every country](https://agentwranglers.se/playbook/localization/text-handling/international-names-and-addresses.md)
- Next: [Normalise text before you compare or store it](https://agentwranglers.se/playbook/localization/text-handling/unicode-normalisation.md)
- [Text handling](https://agentwranglers.se/playbook/localization/text-handling.md)
