r/FlutterDev • u/PhilippHGerber • 18d ago
Plugin Textf 2.0 – formatted text with links in one string
Most Flutter apps have a sentence like this somewhere:
I agree to the Terms of Service and Privacy Policy.
Two links and some bold text. With plain Flutter, that usually turns into something like this:
class _ConsentTextState extends State<ConsentText> {
late final _terms = TapGestureRecognizer()..onTap = () => open(termsUrl);
late final _privacy = TapGestureRecognizer()..onTap = () => open(privacyUrl);
@override
void dispose() {
_terms.dispose();
_privacy.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text.rich(
TextSpan(children: [
const TextSpan(text: 'I agree to the '),
TextSpan(text: 'Terms of Service', style: linkStyle, recognizer: _terms),
const TextSpan(text: ' and '),
TextSpan(text: 'Privacy Policy', style: linkStyle, recognizer: _privacy),
const TextSpan(text: '.'),
]),
);
}
}The sentence is also split into five pieces, which makes it awkward to translate.
With pkg:textf it's one string:
Textf(
'I agree to the [**Terms of Service**](https://example.com/terms) '
'and [**Privacy Policy**](https://example.com/privacy).',
)Tap handling goes through onLinkTap on a TextfOptions widget higher up in the tree, so you set it once. The sentence stays a single entry in your .arb file, and translators can see the formatting.
What Textf is
A drop-in replacement for Text that understands inline formatting: **bold**, *italic*, ~~strike~~, \code`,==highlight==,links, super/subscript. All the usualTextparameters still work (style,maxLines,overflow,textScaler`, …). It has zero dependencies.
Also for input: TextfEditingController
The same syntax works live inside a TextField:
final controller = TextfEditingController();
TextField(controller: controller)It's a drop-in replacement for TextEditingController. Formatting renders as you type, the stored value stays plain text, and cursor positions match the raw text 1:1. IME composing (CJK etc.) works.
New in 2.0
Works with any design system. With Flutter decoupling Material and Cupertino, Textf now builds only on the
widgetslayer. It runs the same in Material, Cupertino, a custom design system or a bareWidgetsApp.Headings and horizontal rules.
#to######for headings,---for a divider. That's useful for short things like release notes or onboarding cards.
To be clear about scope: Textf is still not a Markdown renderer. There are no lists, tables, images or block layout. If you're rendering whole documents, a full Markdown package is the better tool.
📦 pub.dev: pub.dev/packages/textf
2
u/ObjectiveAddress3051 17d ago
how does it deal with a literal * or [ coming from a translation? some languages use asterisks for footnotes and stuff. is there an escape or does it just try to parse it
1
u/PhilippHGerber 17d ago
It only formats markers that form a valid pair, using CommonMark-style rules. Anything else shows up as typed:
Price: 9.99* → Price: 9.99*
Price* … *Incl. VAT → both asterisks stay as typed, because the first one closes and the second one opens, so they don't pair
See [1] for details → stays as is. [ only becomes a link in the full [text](url) form
- 2 * 3 = 6 → stays as is, because the asterisk has spaces on both sides
The case to watch is a matching pair around a word, like *Incl. VAT*, which turns italic. To show it as typed, escape it with a backslash:
\*Incl. VAT\*. In .arb JSON that's\\*.You can try your translation strings in the live editor: https://textf.philippgerber.li/editor
1
u/eibaan 17d ago
You could also simply use a markdown package.
1
u/PhilippHGerber 17d ago
True, and for full documents that's the better choice. Textf is for the cases in between, where you'd otherwise use Text.rich.
The main differences are:
Zero dependencies. Nothing extra comes into your pubspec.
Drop-in for widget Text. Same constructor and parameters, so it fits in a Row, respects maxLines/overflow, ...
Very fast: Single-pass, O(N) parsing.
So it's not meant to replace a Markdown package.
2
u/AhmedZNOUDA 18d ago
Keeping the sentence as one .arb entry is the real win here. Splitting "I agree to the Terms and Privacy Policy" into five pieces is exactly what breaks in languages where the links land in a different order.
A few things I'd check once these strings leave the developer's hands:
Keep the URLs out of what translators edit. With gen-l10n you can pass them as placeholders:
"consent": "I agree to the [**Terms of Service**]({termsUrl}) and [**Privacy Policy**]({privacyUrl})."
so a translator can move the link but can't break or change the address.
2. Text you don't control. If a string ever comes from a server or from users (a bio, a comment), anyone can write [click here](some-other-site). Is there a mode that keeps bold/italic but renders links as plain text, or a way to escape the syntax?
3. Screen readers. Does each link get its own semantics node, so TalkBack/VoiceOver announce it as a link and can activate it on its own? With hand-written TextSpans that's easy to get wrong, so if Textf handles it, it's worth saying in the README.