culture_ui 0.1.0
culture_ui: ^0.1.0 copied to clipboard
Semantic geo-context widgets that automatically adapt UI behavior based on cultural context. Goes beyond translation to handle cultural UX patterns like button placement, layout density, color semanti [...]
culture_ui #
Semantic Geo-Context Widgets for Flutter — automatically adapt UI behavior based on cultural context.
Flutter handles translation (text in different languages) and formatting (dates, numbers via intl). culture_ui handles cultural UX behavior — things like button placement order, dialog button ordering, layout density, color semantics, text emphasis styles, form validation patterns, and name ordering that vary across cultures.
Instead of translating UI, we interpret it culturally.
The Problem #
Localization packages translate text. But UI behavior differs across cultures in ways that go beyond language:
- Button placement: Primary action on the right (US/Europe) vs. left (Arabic/RTL cultures)
- Dialog ordering: Confirm on right (Windows/Android) vs. left (macOS/French convention)
- Layout density: Spacious (Arabic UIs) vs. compact (Japanese/Korean/Chinese UIs)
- Text emphasis: Bold (Western) vs. emphasis dots (Japanese/Chinese)
- Color meanings: White = purity (Western) vs. mourning (Chinese/Indian)
- Name ordering: Given name first (Western) vs. family name first (Japanese/Korean/Chinese)
- Form validation: Postal codes, phone formats, and field labels vary per country
culture_ui provides a single CultureScope widget and a set of Geo* widgets that handle all of this automatically.
Installation #
Add to your pubspec.yaml:
dependencies:
culture_ui: ^0.1.0
Then run:
flutter pub get
Quick Start #
1. Wrap your app with CultureScope #
import 'package:culture_ui/culture_ui.dart';
void main() {
runApp(
CultureScope(
child: MaterialApp(
home: MyHomePage(),
),
),
);
}
CultureScope automatically detects the device locale and resolves the appropriate culture profile. You can also provide an explicit profile:
CultureScope(
profile: CultureProfiles.japanese,
child: MaterialApp(...),
)
2. Use Geo* widgets anywhere in the tree #
// Culture-aware date formatting
GeoDate(date: DateTime.now(), style: GeoDateStyle.long)
// American: "February 8, 2026"
// Japanese: "2026年02月08日"
// German: "8. Februar 2026"
// Culture-aware number formatting
GeoNumber(value: 1234567.89)
// Culture-aware currency
GeoCurrency(amount: 99.99)
// American: "$99.99"
// Japanese: "¥100"
// German: "99,99€"
// Text with cultural emphasis
GeoText('Important', emphasize: true)
// American: bold text
// Japanese: dotted underline
// Buttons with culture-aware ordering
GeoButtonPair(
primary: GeoButtonConfig(label: 'Save', onPressed: _save),
secondary: GeoButtonConfig(label: 'Cancel', onPressed: _cancel),
)
// American: [Cancel] [Save] (primary on right)
// Arabic: [Save] [Cancel] (primary on left)
3. Access the profile directly #
final culture = CultureScope.of(context);
// or with the extension:
final culture = context.culture;
if (culture.readingPattern == ReadingPattern.rtl) {
// Handle RTL-specific logic
}
Widget Reference #
Formatting Widgets #
| Widget | Description | Key Properties |
|---|---|---|
GeoDate |
Culture-aware date display | date, style (short/medium/long), customPattern, includeTime |
GeoNumber |
Culture-aware number display | value, decimalDigits, compact |
GeoCurrency |
Culture-aware currency display | amount, currencyCodeOverride, symbolOverride |
GeoText |
Text with cultural direction & emphasis | data, emphasize, textStyle, maxLines, textAlign |
Layout & Interaction Widgets #
| Widget | Description | Key Properties |
|---|---|---|
GeoButtonPair |
Primary/secondary buttons with culture-aware ordering | primary, secondary (both GeoButtonConfig) |
GeoButton |
Single culture-aware button | config, isPrimary |
GeoDialog |
Dialog with culture-aware button ordering | GeoDialog.show() static method |
GeoLayoutDensity |
Density-appropriate padding wrapper | child, densityOverride, customPadding |
GeoFormField |
Form fields with culture-aware validation | .name(), .postalCode(), .phone() constructors |
Semantic Widgets #
| Widget | Description | Key Properties |
|---|---|---|
GeoColor |
Semantic color resolution | GeoColor.of(context, ColorSemantic.danger) |
GeoIcon |
Culture-specific icon selection | semanticKey, fallback, size, color |
GeoImage |
Culture-specific image selection | images (map by culture ID), fallback |
Utility #
| Class | Description |
|---|---|
GeoSpacing |
Access density-based spacing values via GeoSpacing.of(context) |
FormatUtils |
Static helpers: formatDate(), formatNumber(), formatCurrency(), formatTime() |
Built-in Culture Profiles #
culture_ui ships with 12 built-in profiles, all defined as const for zero runtime allocation cost:
| Profile | Locales | Date Format | Button Position | Density | Dialog Order | Name Order |
|---|---|---|---|---|---|---|
american |
en_US, en | MM/dd/yyyy | Right | Normal | Confirm-Right | Given first |
british |
en_GB | dd/MM/yyyy | Right | Normal | Confirm-Right | Given first |
arabic |
ar, ar_SA, ar_EG | dd/MM/yyyy | Left | Comfortable | Confirm-Left | Given first |
japanese |
ja, ja_JP | yyyy/MM/dd | Right | Compact | Confirm-Right | Family first |
german |
de, de_DE | dd.MM.yyyy | Right | Normal | Confirm-Right | Given first |
indian |
hi, en_IN | dd/MM/yyyy | Right | Normal | Confirm-Right | Given first |
chinese |
zh, zh_CN | yyyy/MM/dd | Right | Compact | Confirm-Right | Family first |
french |
fr, fr_FR | dd/MM/yyyy | Right | Normal | Confirm-Left | Given first |
korean |
ko, ko_KR | yyyy.MM.dd | Right | Compact | Confirm-Right | Family first |
brazilian |
pt_BR, pt | dd/MM/yyyy | Right | Normal | Confirm-Right | Given first |
spanish |
es, es_ES | dd/MM/yyyy | Right | Normal | Confirm-Right | Given first |
russian |
ru, ru_RU | dd.MM.yyyy | Right | Normal | Confirm-Right | Given first |
Access them via CultureProfiles.japanese, CultureProfiles.arabic, etc.
Creating Custom Profiles #
Create your own culture profile by instantiating CultureProfile:
const thai = CultureProfile(
id: 'thai',
locales: ['th', 'th_TH'],
displayName: 'Thai',
readingPattern: ReadingPattern.ltr,
textEmphasisStyle: TextEmphasisStyle.bold,
dateFormatPattern: 'dd/MM/yyyy',
currencyCode: 'THB',
currencySymbol: '฿',
currencySymbolPosition: CurrencySymbolPosition.before,
layoutDensity: LayoutDensity.normal,
colorSemantics: {
ColorSemantic.luck: Color(0xFFFFD700), // Gold
ColorSemantic.mourning: Color(0xFF212121), // Black
},
);
Register it for automatic locale detection:
CultureDetector.instance.registerProfile(thai);
Or modify an existing profile with copyWith:
final customAmerican = CultureProfiles.american.copyWith(
layoutDensity: LayoutDensity.compact,
textEmphasisStyle: TextEmphasisStyle.underline,
);
Usage Examples #
Culture-Aware Dialogs #
final confirmed = await GeoDialog.show(
context: context,
title: 'Delete Account',
message: 'This action cannot be undone. Continue?',
confirmLabel: 'Delete',
cancelLabel: 'Keep',
);
// Button order adapts: [Keep] [Delete] (US) vs [Delete] [Keep] (French)
Semantic Colors #
Container(
color: GeoColor.of(context, ColorSemantic.luck),
// Red in Japanese/Chinese culture, Gold in Indian culture
)
Culture-Aware Form Fields #
Form(
child: Column(
children: [
GeoFormField.name(controller: nameController),
// Label: "Full Name" (US) vs "Family Name" (Japan)
GeoFormField.postalCode(controller: postalController),
// Validates: 12345 (US) vs 123-4567 (Japan) vs A1B 2C3 (UK)
GeoFormField.phone(controller: phoneController),
],
),
)
Layout Density #
GeoLayoutDensity(
child: MyContentWidget(),
)
// Padding: 8px (Japanese/compact) vs 16px (American/normal) vs 24px (Arabic/comfortable)
Override Culture for a Subtree #
CultureScope(
profile: CultureProfiles.japanese,
child: Column(
children: [
GeoDate(date: DateTime.now()), // Uses Japanese format
GeoText('Preview', emphasize: true), // Uses emphasis dots
],
),
)
Culture-Specific Icons and Images #
// Icons that change per culture
GeoIcon(
semanticKey: 'home',
fallback: Icons.home,
)
// Images that change per culture
GeoImage(
images: {
'american': AssetImage('assets/hero_us.png'),
'japanese': AssetImage('assets/hero_jp.png'),
},
fallback: AssetImage('assets/hero_default.png'),
)
Architecture #
culture_ui follows Flutter's own Theme/ThemeData pattern:
CultureScope (like Theme)
└── Provides CultureProfile (like ThemeData) via InheritedWidget
└── Geo* widgets read profile from context and adapt behavior
- CultureProfile — Immutable
@immutabledata class holding all cultural config - CultureScope —
InheritedWidgetwrapper withof(context)andmaybeOf(context) - CultureDetector — Resolves device
Localeto the best matchingCultureProfile - ProfileRegistry — Manages profile lookup with support for custom profiles
- Geo* widgets — Read
CultureScope.of(context)and render culture-appropriate UI
Only dependency: intl (for DateFormat and NumberFormat).
CultureProfile Fields #
The CultureProfile class contains configuration for:
| Category | Fields |
|---|---|
| Identity | id, locales, displayName |
| Text | readingPattern, textEmphasisStyle, truncationDirection |
| Date/Time | dateFormatPattern, shortDatePattern, longDatePattern, timeFormatPattern, uses24HourTime |
| Numbers | numberLocale |
| Currency | currencyCode, currencySymbol, currencySymbolPosition, currencyDecimalDigits |
| Layout | primaryButtonPosition, dialogButtonOrder, layoutDensity, formLayoutPreference, defaultPadding, compactPadding, comfortablePadding |
| Semantics | colorSemantics (Map<ColorSemantic, Color>), iconOverrides (Map<String, IconData>) |
| Forms | nameInputExpectsFamily, postalCodePattern, phoneNumberPattern |
Enums #
| Enum | Values |
|---|---|
ReadingPattern |
ltr, rtl, topToBottom |
LayoutDensity |
compact, normal, comfortable |
TextEmphasisStyle |
bold, emphasisDots, underline, enlargedSize |
PrimaryButtonPosition |
left, right |
DialogButtonOrder |
confirmRight, confirmLeft |
ColorSemantic |
danger, success, warning, info, luck, mourning, celebration, purity, love, wealth, authority |
CurrencySymbolPosition |
before, after |
NameOrder |
givenFirst, familyFirst |
TruncationDirection |
end, start, middle |
FormLayoutPreference |
labelAbove, labelLeft, floatingLabel |
Testing #
The package includes 128 tests covering all core classes, profiles, and widgets.
cd culture_ui
flutter test
Use the wrapWithCulture() test helper to wrap widgets with a specific profile in tests:
import 'package:culture_ui/culture_ui.dart';
Widget wrapWithCulture(Widget child, {required CultureProfile profile}) {
return MaterialApp(
home: CultureScope(profile: profile, child: Scaffold(body: child)),
);
}
testWidgets('shows Japanese date format', (tester) async {
await tester.pumpWidget(
wrapWithCulture(
GeoDate(date: DateTime(2026, 2, 8)),
profile: CultureProfiles.japanese,
),
);
expect(find.text('2026/02/08'), findsOneWidget);
});
Example App #
A full example app is included in the example/ directory with a culture-switcher dropdown and demos for all widgets.
cd example
flutter run
Requirements #
- Dart SDK:
^3.9.0 - Flutter:
>=3.10.0 - Dependencies:
intl ^0.19.0
License #
MIT License. See LICENSE for details.
Author #
Muhammad Umer
- GitHub: @Umer9538
- Email: muhammadumer9538@gmail.com