diff --git a/.pubignore b/.pubignore
new file mode 100644
index 00000000..8896c6c5
--- /dev/null
+++ b/.pubignore
@@ -0,0 +1,11 @@
+assets/*.png
+assets/css/
+build/
+.dart_tool/
+.idea/
+.github/
+SINT_GETX_COMPARISON.docx
+SINT_GETX_COMPARISON.md
+SINT_GETX_EVOLUTION_BRIEF.md
+_config.yml
+index.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 979c8f46..ca7d7bd7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,44 @@
+## [1.2.1] - 2026-02-28
+
+- **README images**: Switched to absolute GitHub raw URLs so images render correctly on pub.dev (`.pubignore` excludes PNGs from the package).
+
+---
+
+## [1.2.0] - 2026-02-28
+
+RESTful Navigation & i18n URL Routing.
+
+133 lines of new code. Zero new dependencies. Pillars N and T upgraded.
+
+### Pillar N (Navigation)
+
+- **RESTful Route Parameters** (Spring Boot-inspired API):
+ - `Sint.routeParam` — Primary path parameter value. For route `/book/:bookId` navigated as `/book/abc123`, returns `'abc123'`. Equivalent to Spring Boot's `@PathVariable`.
+ - `Sint.pathParam('bookId')` — Named path parameter. Equivalent to `@PathVariable("bookId")`.
+ - `Sint.queryParam('page')` — Query parameter from URL. Equivalent to `@RequestParam`.
+ - `Sint.queryParamOrDefault('sort', 'recent')` — Query parameter with fallback. Equivalent to `@RequestParam(defaultValue = "recent")`.
+ - Full test mode support via `SintTestMode`.
+- **`translateEndpoints` flag**: New parameter on `SintMaterialApp` and `ConfigData` that enables automatic i18n URL routing. When `true`, SINT builds a `PathTranslator` from registered translations and routes.
+- **`setUrlStrategy()` resilience**: Wrapped in try-catch to handle "URL strategy already set" when the Flutter engine is already initialized — prevents web startup crashes on hot restart.
+
+### Pillar T (Translation)
+
+- **`PathTranslator`** — New class for internationalized URL routing:
+ - `canonicalizePath()` — Converts localized URLs to canonical English before route matching. e.g. `/libro/abc123` → `/book/abc123`.
+ - `localizePath()` — Converts canonical URLs to the current locale for the browser URL bar. e.g. `/book/abc123` → `/libro/abc123` (ES) or `/livre/abc123` (FR).
+ - `extractSegments()` — Automatically extracts static route segments from registered `SintPage` names (skips `:param` segments).
+ - Built-in diacritics normalization (`Publicación` → `publicacion`) for clean URLs.
+ - Zero-config: built automatically from existing app translations when `translateEndpoints: true`. No external localization file needed.
+- **`Sint.pathTranslator`** — Getter/setter on the `SintInterface` to access the URL translator. Stored in `IntlHost` and cleaned up on `SintRoot.onClose()`.
+- **`SintInformationParser` integration** — Automatic canonicalization on `parseRouteInformation()` and localization on `restoreRouteInformation()`. Browser URL bar shows localized paths; internal routing uses canonical English.
+
+### Housekeeping
+
+- **Example app**: Added `example/main.dart` demonstrating all four SINT pillars (State, Injection, Navigation, Translation) in a counter app. Targets 160/160 pub points.
+- **TickerMode.of deprecation**: Suppressed for cross-SDK compatibility in `RxTickerProviderMixin`.
+
+---
+
## [1.1.0] - 2026-02-26
The Four Pillars Evolve — Workers, Pattern Matching, Async DI & Web-Safe Navigation.
diff --git a/README.md b/README.md
index 2f64464e..bbb1e5d3 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# SINT
+
+
+
+
**State, Injection, Navigation, Translation — The Four Pillars of High-Fidelity Flutter Infrastructure.**
[](https://pub.dev/packages/sint)
@@ -26,6 +30,7 @@
---
- [About SINT](#about-sint)
+- [What's New in 1.2.0](#whats-new-in-120)
- [What's New in 1.1.0](#whats-new-in-110)
- [Installing](#installing)
- [The Four Pillars](#the-four-pillars)
@@ -33,6 +38,7 @@
- [Injection (I)](#injection-i)
- [Navigation (N)](#navigation-n)
- [Translation (T)](#translation-t)
+- [Flutter Web & Deep Links](#flutter-web--deep-links)
- [Counter App with SINT](#counter-app-with-sint)
- [Migration from GetX](#migration-from-getx)
- [Origin & Philosophy](#origin--philosophy)
@@ -47,8 +53,8 @@ SINT is an architectural evolution of GetX (v5.0.0-rc), built as a focused frame
|---|---|
| **S** — State Management | `SintController`, `SintBuilder`, `Obx`, `.obs`, Rx types, Workers, `SintStatus`, `SintListener` |
| **I** — Injection | `Sint.put`, `Sint.find`, `Sint.lazyPut`, `Sint.putAsync`, Bindings, SmartManagement |
-| **N** — Navigation | `SintPage`, `Sint.toNamed`, `Sint.toInitial`, middleware, `SintMaterialApp`, web-safe `back()` |
-| **T** — Translation | `.tr` extension, `Translations` class, locale management, `loadTranslations` |
+| **N** — Navigation | `SintPage`, `Sint.toNamed`, `Sint.toInitial`, `routeParam`, `pathParam`, `queryParam`, middleware, `SintMaterialApp`, `SintSnackBarStyle`, web-safe `back()` |
+| **T** — Translation | `.tr` extension, `Translations` class, locale management, `loadTranslations`, `PathTranslator`, `translateEndpoints` |
Everything outside these four pillars has been removed: no HTTP client, no animations, no string validators, no generic utilities. The result is **37.7% less code** than GetX — 12,849 LOC vs 20,615 LOC.
@@ -60,6 +66,109 @@ Everything outside these four pillars has been removed: no HTTP client, no anima
---
+## What's New in 1.2.0
+
+**Focus: Flutter Web, Deep Links & i18n URL Routing — without breaking mobile.**
+
+### RESTful Route Parameters
+
+Spring Boot-inspired parameter extraction that works identically on mobile and web:
+
+```dart
+// Define routes with path parameters (same as before)
+SintPage(name: '/book/:bookId', page: () => BookDetail()),
+SintPage(name: '/shop/product/:productId', page: () => ProductPage()),
+
+// Navigate (works on all platforms)
+Sint.toNamed('/book/abc123');
+Sint.toNamed('/shop/product/42?color=red&size=lg');
+
+// Extract parameters — clean API, no manual parsing
+String? bookId = Sint.routeParam; // 'abc123'
+String? productId = Sint.pathParam('productId'); // '42'
+String? color = Sint.queryParam('color'); // 'red'
+String size = Sint.queryParamOrDefault('size', 'm'); // 'lg'
+```
+
+| Method | Equivalent (Spring Boot) | Description |
+|--------|--------------------------|-------------|
+| `Sint.routeParam` | `@PathVariable` | First path parameter value |
+| `Sint.pathParam('id')` | `@PathVariable("id")` | Named path parameter |
+| `Sint.queryParam('q')` | `@RequestParam` | Query string parameter |
+| `Sint.queryParamOrDefault('sort', 'asc')` | `@RequestParam(defaultValue)` | Query with fallback |
+
+All four methods support `SintTestMode` for unit testing without a running app.
+
+### i18n URL Routing (translateEndpoints)
+
+Localized URLs in the browser address bar — zero configuration beyond what you already have:
+
+```dart
+SintMaterialApp(
+ translateEndpoints: true, // Enable URL localization
+ translationsKeys: AppTranslations.keys,
+ locale: Locale('es'),
+ sintPages: [
+ SintPage(name: '/book/:bookId', page: () => BookDetail()),
+ SintPage(name: '/event/:eventId', page: () => EventDetail()),
+ ],
+)
+```
+
+Your existing translations automatically power the URL routing:
+
+```dart
+// In your translations file — no extra config needed
+'es': { 'book': 'libro', 'event': 'evento', ... }
+'fr': { 'book': 'livre', 'event': 'evenement', ... }
+'de': { 'book': 'buch', 'event': 'veranstaltung', ... }
+```
+
+Result:
+
+| Locale | Browser URL | Internal Route |
+|--------|-------------|----------------|
+| EN | `/book/abc123` | `/book/abc123` |
+| ES | `/libro/abc123` | `/book/abc123` |
+| FR | `/livre/abc123` | `/book/abc123` |
+| DE | `/buch/abc123` | `/book/abc123` |
+
+**How it works:**
+
+1. `PathTranslator` is built automatically from your registered routes + translations
+2. Incoming URLs are canonicalized before route matching (`/libro/x` → `/book/x`)
+3. Outgoing URLs are localized for the browser bar (`/book/x` → `/libro/x`)
+4. Diacritics are normalized automatically (`Publicación` → `publicacion`)
+5. On mobile, `translateEndpoints` has zero overhead — path translation only activates for web URL parsing
+
+### Global Snackbar Theming
+
+Define snackbar appearance once, apply everywhere:
+
+```dart
+SintMaterialApp(
+ snackBarStyle: SintSnackBarStyle(
+ backgroundColor: Colors.grey[900],
+ colorText: Colors.white,
+ borderRadius: 12,
+ margin: EdgeInsets.all(16),
+ snackPosition: SnackPosition.bottom,
+ duration: Duration(seconds: 3),
+ ),
+)
+
+// All snackbar calls inherit the global style
+Sint.snackbar('Title', 'Message');
+// Call-site params still override when needed
+Sint.snackbar('Error', 'Failed', backgroundColor: Colors.red);
+```
+
+Three-level cascade: **call-site > global style > hardcoded defaults**.
+
+See [CHANGELOG.md](CHANGELOG.md) for the full list of changes.
+
+---
+
## What's New in 1.1.0
### Reactive Workers
@@ -146,7 +255,7 @@ Add SINT to your `pubspec.yaml`:
```yaml
dependencies:
- sint: ^1.1.0
+ sint: ^1.2.0
```
Import it:
@@ -180,6 +289,10 @@ SINT is built for speed. Every pillar is audited against the Open Neom Standard.
## The Four Pillars
+
+
+
+
### State Management (S)
Two approaches: **Reactive** (`.obs` + `Obx`) and **Simple** (`SintBuilder`).
@@ -233,21 +346,33 @@ final controller = Sint.find();
### Navigation (N)
-Route management without context:
+Route management without context — optimized for web deep links and mobile alike:
```dart
SintMaterialApp(
initialRoute: '/',
+ translateEndpoints: true, // i18n URLs (web)
+ snackBarStyle: SintSnackBarStyle(...), // Global theming
sintPages: [
SintPage(name: '/', page: () => Home()),
- SintPage(name: '/details', page: () => Details()),
+ SintPage(name: '/book/:bookId', page: () => BookDetail()),
+ SintPage(name: '/search', page: () => Search()),
],
)
-Sint.toNamed('/details');
+// Navigation
+Sint.toNamed('/book/abc123?ref=home');
Sint.back(); // Web-safe
Sint.toInitial(); // Hard reset to home
Sint.toInitial(keep: {AuthController}); // Keep specific controllers
+
+// RESTful parameter extraction
+String? id = Sint.routeParam; // 'abc123'
+String? id = Sint.pathParam('bookId'); // 'abc123'
+String? ref = Sint.queryParam('ref'); // 'home'
+String sort = Sint.queryParamOrDefault('sort', 'a'); // 'a' (default)
+
+// Snackbar with global style
Sint.snackbar('Title', 'Message');
```
@@ -255,7 +380,7 @@ Sint.snackbar('Title', 'Message');
### Translation (T)
-Internationalization with `.tr`:
+Internationalization with `.tr` — now powers URL routing too:
```dart
Text('hello'.tr);
@@ -267,12 +392,76 @@ await Sint.loadTranslations(() async {
final json = await rootBundle.loadString('assets/i18n/shop.json');
return {'es': Map.from(jsonDecode(json))};
});
+
+// URL path translation (automatic when translateEndpoints: true)
+// Your translation keys double as URL segment mappings:
+// 'book' → 'libro' (ES), 'livre' (FR), 'buch' (DE)
+//
+// PathTranslator handles:
+// canonicalizePath('/libro/abc') → '/book/abc' (incoming)
+// localizePath('/book/abc', 'es') → '/libro/abc' (outgoing)
```
[Full documentation](documentation/en_US/translation_management.md)
---
+## Flutter Web & Deep Links
+
+SINT is designed with a **web-first, mobile-safe** philosophy. Every feature works identically across platforms, but web gets extra optimizations:
+
+| Feature | Web Behavior | Mobile Behavior |
+|---------|-------------|-----------------|
+| `Sint.back()` | No-op if no internal history (browser arrows handle it) | Standard `Navigator.pop()` |
+| `Sint.routeParam` | Extracted from browser URL path | Extracted from route arguments |
+| `Sint.queryParam()` | Extracted from URL query string `?key=value` | Extracted from route arguments |
+| `translateEndpoints` | Localizes browser URL bar + canonicalizes incoming URLs | No overhead — flag is ignored |
+| `Sint.showBackButton` | `false` (browser has native arrows) | `true` |
+| Default transition | `Transition.fade` (GPU-light for web canvas) | Platform default (Cupertino/Material) |
+| Scroll behavior | Drag enabled for touch, mouse, and trackpad | Platform default |
+| `SintSnackBarStyle` | Same styling across web and mobile | Same styling across web and mobile |
+
+### Deep Link Example (Web + Mobile)
+
+```dart
+// 1. Define routes with parameters
+SintMaterialApp(
+ initialRoute: '/',
+ translateEndpoints: true,
+ translationsKeys: AppTranslations.keys,
+ locale: Locale('es'),
+ sintPages: [
+ SintPage(name: '/', page: () => HomePage()),
+ SintPage(name: '/book/:bookId', page: () => BookDetail()),
+ SintPage(name: '/profile/:userId', page: () => ProfilePage()),
+ ],
+)
+
+// 2. In your controller — same code works everywhere
+class BookDetailController extends SintController {
+ late final String bookId;
+
+ @override
+ void onInit() {
+ super.onInit();
+ // Works from: browser URL, deep link, or Sint.toNamed()
+ bookId = Sint.routeParam ?? '';
+ loadBook(bookId);
+ }
+}
+```
+
+**On web:** User visits `https://myapp.com/libro/abc123` →
+SINT canonicalizes to `/book/abc123` → routes to `BookDetail` →
+`Sint.routeParam` returns `'abc123'` → browser shows `/libro/abc123`.
+
+**On mobile:** `Sint.toNamed('/book/abc123')` →
+routes to `BookDetail` → `Sint.routeParam` returns `'abc123'`.
+
+**Same controller. Same routes. Same parameters. Zero platform checks.**
+
+---
+
## Counter App with SINT
```dart
diff --git a/assets/SINT - Framework - 2026.png b/assets/SINT - Framework - 2026.png
new file mode 100644
index 00000000..6dca6eb2
Binary files /dev/null and b/assets/SINT - Framework - 2026.png differ
diff --git a/assets/SINT - Logo - 2026.png b/assets/SINT - Logo - 2026.png
new file mode 100644
index 00000000..f0e4f11e
Binary files /dev/null and b/assets/SINT - Logo - 2026.png differ
diff --git a/lib/navigation/src/domain/extensions/navigation_extensions.dart b/lib/navigation/src/domain/extensions/navigation_extensions.dart
index 8e743a19..d439fc5e 100644
--- a/lib/navigation/src/domain/extensions/navigation_extensions.dart
+++ b/lib/navigation/src/domain/extensions/navigation_extensions.dart
@@ -906,6 +906,37 @@ extension NavigationExtension on SintInterface {
return rootController.rootDelegate.parameters;
}
+ /// Primary route parameter from URL path. Returns null if none.
+ /// For route '/book/:bookId' navigated as '/book/abc123', returns 'abc123'.
+ /// Usage: `String? id = Sint.routeParam;`
+ String? get routeParam {
+ if (_shouldUseMock) return SintTestMode.routeParam;
+ return rootController.rootDelegate.routeParam;
+ }
+
+ /// Named path parameter (like Spring Boot @PathVariable).
+ /// Usage: `String? id = Sint.pathParam('bookId');`
+ String? pathParam(String name) {
+ if (_shouldUseMock) return SintTestMode.pathParam(name);
+ return rootController.rootDelegate.pathParam(name);
+ }
+
+ /// Query parameter from URL (like Spring Boot @RequestParam).
+ /// Usage: `String? page = Sint.queryParam('page');`
+ String? queryParam(String name) {
+ if (_shouldUseMock) return SintTestMode.queryParam(name);
+ return rootController.rootDelegate.queryParam(name);
+ }
+
+ /// Query parameter with default value.
+ /// Usage: `String sort = Sint.queryParamOrDefault('sort', 'recent');`
+ String queryParamOrDefault(String name, String defaultValue) {
+ if (_shouldUseMock) {
+ return SintTestMode.queryParamOrDefault(name, defaultValue);
+ }
+ return rootController.rootDelegate.queryParamOrDefault(name, defaultValue);
+ }
+
/// Casts the stored router delegate to a desired type
TDelegate? delegate, TPage>() =>
_getxController.routerDelegate as TDelegate?;
diff --git a/lib/navigation/src/domain/models/config_data.dart b/lib/navigation/src/domain/models/config_data.dart
index ad7da160..8336a7d5 100644
--- a/lib/navigation/src/domain/models/config_data.dart
+++ b/lib/navigation/src/domain/models/config_data.dart
@@ -55,6 +55,7 @@ class ConfigData {
final Routing routing;
final Map parameters;
final SintSnackBarStyle? snackBarStyle;
+ final bool translateEndpoints;
final SnackBarQueue snackBarQueue = SnackBarQueue();
ConfigData({
@@ -98,6 +99,7 @@ class ConfigData {
this.parameters = const {},
required this.defaultPopGesture,
this.snackBarStyle,
+ this.translateEndpoints = false,
Routing? routing,
}) : routing = routing ?? Routing();
@@ -141,6 +143,7 @@ class ConfigData {
Curve? defaultDialogTransitionCurve,
Duration? defaultDialogTransitionDuration,
SintSnackBarStyle? snackBarStyle,
+ bool? translateEndpoints,
Routing? routing,
Map? parameters,
}) {
@@ -191,6 +194,7 @@ class ConfigData {
defaultDialogTransitionDuration: defaultDialogTransitionDuration ??
this.defaultDialogTransitionDuration,
snackBarStyle: snackBarStyle ?? this.snackBarStyle,
+ translateEndpoints: translateEndpoints ?? this.translateEndpoints,
routing: routing ?? this.routing,
parameters: parameters ?? this.parameters,
);
@@ -243,6 +247,7 @@ class ConfigData {
defaultDialogTransitionDuration &&
other.routing == routing &&
other.snackBarStyle == snackBarStyle &&
+ other.translateEndpoints == translateEndpoints &&
mapEquals(other.parameters, parameters);
}
@@ -289,6 +294,7 @@ class ConfigData {
defaultDialogTransitionDuration.hashCode ^
routing.hashCode ^
snackBarStyle.hashCode ^
+ translateEndpoints.hashCode ^
parameters.hashCode;
}
}
\ No newline at end of file
diff --git a/lib/navigation/src/router/sint_delegate.dart b/lib/navigation/src/router/sint_delegate.dart
index b393c962..aa11b7d5 100644
--- a/lib/navigation/src/router/sint_delegate.dart
+++ b/lib/navigation/src/router/sint_delegate.dart
@@ -90,7 +90,13 @@ class SintDelegate extends RouterDelegate
body: Center(child: Text('Route not found')),
),
) {
- if (!showHashOnUrl && kIsWeb) setUrlStrategy();
+ if (!showHashOnUrl && kIsWeb) {
+ try {
+ setUrlStrategy();
+ } catch (_) {
+ // URL strategy already set or engine already initialized — safe to ignore.
+ }
+ }
addPages(pages);
addPage(notFoundRoute);
Sint.log('GetDelegate is created !');
@@ -162,6 +168,37 @@ class SintDelegate extends RouterDelegate
return currentConfiguration?.pageSettings;
}
+ /// Primary route path parameter value (first path param).
+ /// For route '/book/:bookId' navigated as '/book/abc123', returns 'abc123'.
+ /// Returns null if no path parameter exists.
+ /// Inspired by Spring Boot's @PathVariable annotation.
+ String? get routeParam {
+ final ps = currentConfiguration?.pageSettings;
+ if (ps == null) return null;
+ final allParams = ps.params;
+ if (allParams.isEmpty) return null;
+ final queryKeys = ps.query.keys.toSet();
+ for (final entry in allParams.entries) {
+ if (!queryKeys.contains(entry.key)) return entry.value;
+ }
+ return null;
+ }
+
+ /// Named path parameter (like Spring Boot @PathVariable("bookId")).
+ String? pathParam(String name) {
+ return currentConfiguration?.pageSettings?.params[name];
+ }
+
+ /// Query parameter only (like Spring Boot @RequestParam).
+ String? queryParam(String name) {
+ return currentConfiguration?.pageSettings?.query[name];
+ }
+
+ /// Query parameter with default (like @RequestParam(defaultValue = "10")).
+ String queryParamOrDefault(String name, String defaultValue) {
+ return currentConfiguration?.pageSettings?.query[name] ?? defaultValue;
+ }
+
Future _pushHistory(RouteDecoder config) async {
if (config.route!.preventDuplicates) {
final originalEntryIndex = _activePages.indexWhere(
diff --git a/lib/navigation/src/router/sint_information_parser.dart b/lib/navigation/src/router/sint_information_parser.dart
index 0f287b92..ed4120a4 100644
--- a/lib/navigation/src/router/sint_information_parser.dart
+++ b/lib/navigation/src/router/sint_information_parser.dart
@@ -34,6 +34,12 @@ class SintInformationParser extends RouteInformationParser {
location = initialRoute;
}
+ // URL Canonicalization: normalize localized segments to canonical English
+ // e.g. '/libro/abc123' → '/book/abc123' before route matching
+ if (Sint.pathTranslator != null) {
+ location = Sint.pathTranslator!.canonicalizePath(location);
+ }
+
Sint.log('GetInformationParser: route location: $location');
return SynchronousFuture(RouteDecoder.fromRoute(location));
@@ -41,8 +47,16 @@ class SintInformationParser extends RouteInformationParser {
@override
RouteInformation restoreRouteInformation(RouteDecoder configuration) {
+ var name = configuration.pageSettings?.name ?? '';
+
+ // URL Localization: translate canonical segments to current locale
+ // e.g. '/book/abc123' → '/libro/abc123' for the browser URL bar
+ if (Sint.pathTranslator != null && Sint.locale != null) {
+ name = Sint.pathTranslator!.localizePath(name, Sint.locale!.languageCode);
+ }
+
return RouteInformation(
- uri: Uri.tryParse(configuration.pageSettings?.name ?? ''),
+ uri: Uri.tryParse(name),
state: null,
);
}
diff --git a/lib/navigation/src/router/sint_test_mode.dart b/lib/navigation/src/router/sint_test_mode.dart
index 3296cd60..0688d16e 100644
--- a/lib/navigation/src/router/sint_test_mode.dart
+++ b/lib/navigation/src/router/sint_test_mode.dart
@@ -15,4 +15,18 @@ class SintTestMode {
}
static Map get parameters => _parameters;
+
+ /// Test mode support for routeParam — returns first parameter value.
+ static String? get routeParam =>
+ _parameters.values.where((v) => v != null && v.isNotEmpty).firstOrNull;
+
+ /// Test mode support for named path parameter.
+ static String? pathParam(String name) => _parameters[name];
+
+ /// Test mode support for query parameter.
+ static String? queryParam(String name) => null;
+
+ /// Test mode support for query parameter with default.
+ static String queryParamOrDefault(String name, String defaultValue) =>
+ defaultValue;
}
diff --git a/lib/navigation/src/ui/sint_material_app.dart b/lib/navigation/src/ui/sint_material_app.dart
index cc9e2193..f3f482bb 100644
--- a/lib/navigation/src/ui/sint_material_app.dart
+++ b/lib/navigation/src/ui/sint_material_app.dart
@@ -75,6 +75,7 @@ class SintMaterialApp extends StatelessWidget {
final BackButtonDispatcher? backButtonDispatcher;
final SintSnackBarStyle? snackBarStyle;
final bool useInheritedMediaQuery;
+ final bool translateEndpoints;
const SintMaterialApp({
super.key,
@@ -134,6 +135,7 @@ class SintMaterialApp extends StatelessWidget {
this.highContrastTheme,
this.highContrastDarkTheme,
this.actions,
+ this.translateEndpoints = false,
}) : routeInformationProvider = null,
backButtonDispatcher = null,
routeInformationParser = null,
@@ -194,6 +196,7 @@ class SintMaterialApp extends StatelessWidget {
this.navigatorObservers,
this.unknownRoute,
this.snackBarStyle,
+ this.translateEndpoints = false,
}) : navigatorKey = null,
onGenerateRoute = null,
// ignore: deprecated_member_use_from_same_package
@@ -240,6 +243,7 @@ class SintMaterialApp extends StatelessWidget {
themeMode: themeMode,
defaultPopGesture: popGesture,
snackBarStyle: snackBarStyle,
+ translateEndpoints: translateEndpoints,
),
child: Builder(builder: (context) {
final controller = SintRoot.of(context);
diff --git a/lib/navigation/src/ui/sint_root.dart b/lib/navigation/src/ui/sint_root.dart
index f15e8847..af229495 100644
--- a/lib/navigation/src/ui/sint_root.dart
+++ b/lib/navigation/src/ui/sint_root.dart
@@ -61,6 +61,7 @@ class SintRootState extends State with WidgetsBindingObserver {
void onClose() {
config.onDispose?.call();
Sint.clearTranslations();
+ Sint.pathTranslator = null;
config.snackBarQueue.disposeControllers();
RouterReportManager.instance.clearRouteKeys();
RouterReportManager.dispose();
@@ -133,6 +134,18 @@ class SintRootState extends State with WidgetsBindingObserver {
Sint.addTranslations(config.translationsKeys!);
}
+ // Build path translator for URL segment localization (web).
+ if (config.translateEndpoints) {
+ final delegate = config.routerDelegate as SintDelegate;
+ final segments = PathTranslator.extractSegments(
+ delegate.registeredRoutes,
+ );
+ Sint.pathTranslator = PathTranslator.build(
+ translations: Sint.translations,
+ routeSegments: segments,
+ );
+ }
+
Sint.smartManagement = config.smartManagement;
config.onInit?.call();
diff --git a/lib/translation/sint_translation.dart b/lib/translation/sint_translation.dart
index 5fe4fbed..afb4412e 100644
--- a/lib/translation/sint_translation.dart
+++ b/lib/translation/sint_translation.dart
@@ -4,4 +4,5 @@ export 'src/domain/extensions/locale_extension.dart';
export 'src/domain/extensions/trans_extension.dart';
export 'src/domain/interfaces/translations.dart';
export 'src/domain/models/intl_host.dart';
+export 'src/domain/models/path_translator.dart';
export 'src/utils/translations_constants.dart';
diff --git a/lib/translation/src/domain/extensions/locale_extension.dart b/lib/translation/src/domain/extensions/locale_extension.dart
index 2d39d192..27462636 100644
--- a/lib/translation/src/domain/extensions/locale_extension.dart
+++ b/lib/translation/src/domain/extensions/locale_extension.dart
@@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:sint/core/src/domain/interfaces/sint_interface.dart';
import 'package:sint/translation/src/domain/models/intl_host.dart';
+import 'package:sint/translation/src/domain/models/path_translator.dart';
extension LocalesIntl on SintInterface {
static final _intlHost = IntlHost();
@@ -16,6 +17,11 @@ extension LocalesIntl on SintInterface {
Map> get translations => _intlHost.translations;
+ /// URL path translator (built when `translateEndpoints: true`).
+ PathTranslator? get pathTranslator => _intlHost.pathTranslator;
+
+ set pathTranslator(PathTranslator? value) => _intlHost.pathTranslator = value;
+
void addTranslations(Map> tr) {
translations.addAll(tr);
}
diff --git a/lib/translation/src/domain/models/intl_host.dart b/lib/translation/src/domain/models/intl_host.dart
index 2d6bd7b9..dcb6401d 100644
--- a/lib/translation/src/domain/models/intl_host.dart
+++ b/lib/translation/src/domain/models/intl_host.dart
@@ -1,9 +1,14 @@
import 'dart:ui';
+import 'path_translator.dart';
+
class IntlHost {
Locale? locale;
Locale? fallbackLocale;
Map> translations = {};
+
+ /// URL path translator built from translations when `translateEndpoints` is enabled.
+ PathTranslator? pathTranslator;
}
\ No newline at end of file
diff --git a/lib/translation/src/domain/models/path_translator.dart b/lib/translation/src/domain/models/path_translator.dart
new file mode 100644
index 00000000..f5ab3047
--- /dev/null
+++ b/lib/translation/src/domain/models/path_translator.dart
@@ -0,0 +1,161 @@
+/// Translates URL path segments between canonical (English) and localized forms.
+///
+/// Built automatically by SINT when `translateEndpoints: true` is set on
+/// [SintMaterialApp]. Uses the app's registered translations to derive
+/// segment mappings — no external localization file needed.
+///
+/// Example:
+/// - ES: `/book/abc123` ↔ `/libro/abc123`
+/// - FR: `/book/abc123` ↔ `/livre/abc123`
+class PathTranslator {
+ /// Per-locale forward maps: canonical segment → localized segment.
+ /// e.g. `{'es': {'book': 'libro', 'event': 'evento'}, 'fr': {'book': 'livre'}}`
+ final Map> _forwardMaps;
+
+ /// Reverse map: any localized segment → canonical segment (all locales).
+ /// e.g. `{'libro': 'book', 'livre': 'book', 'buch': 'book'}`
+ final Map _reverseMap;
+
+ PathTranslator._(this._forwardMaps, this._reverseMap);
+
+ /// Builds a [PathTranslator] from SINT's loaded translations and the
+ /// static route segments extracted from registered [SintPage] names.
+ ///
+ /// [translations] — full translation map from `Sint.translations`
+ /// (`{locale: {key: value}}`).
+ /// [routeSegments] — static segments extracted via [extractSegments].
+ factory PathTranslator.build({
+ required Map> translations,
+ required Set routeSegments,
+ }) {
+ final forwardMaps = >{};
+ final reverseMap = {};
+
+ for (final localeEntry in translations.entries) {
+ // Handle both 'es' and 'es_MX' style keys.
+ final locale = localeEntry.key.split('_').first;
+ if (locale == 'en') continue; // English is canonical — no mapping.
+
+ final localeMap = {};
+ for (final segment in routeSegments) {
+ final value = localeEntry.value[segment];
+ if (value == null) continue;
+
+ final normalized = _removeDiacritics(value.toLowerCase());
+ if (normalized.contains(' ')) continue; // Multi-word: can't be URL segment.
+ if (normalized == segment) continue; // Same as canonical: no-op.
+
+ localeMap[segment] = normalized;
+ reverseMap[normalized] = segment;
+ }
+
+ if (localeMap.isNotEmpty) {
+ // Merge into existing locale map (handles 'es' + 'es_MX' both present).
+ forwardMaps.putIfAbsent(locale, () => {}).addAll(localeMap);
+ }
+ }
+
+ return PathTranslator._(forwardMaps, reverseMap);
+ }
+
+ /// Extracts unique static segments from registered route names.
+ ///
+ /// `/book/:bookId` → `{'book'}`
+ /// `/shop/product/:productId` → `{'shop', 'product'}`
+ static Set extractSegments(List routes) {
+ final segments = {};
+ for (final route in routes) {
+ final name = (route as dynamic).name as String;
+ for (final seg in name.split('/')) {
+ if (seg.isEmpty) continue;
+ if (seg.startsWith(':')) continue; // Skip parameters.
+ segments.add(seg);
+ }
+ }
+ return segments;
+ }
+
+ // ─── Public API ───────────────────────────────────────────────
+
+ /// Canonicalizes a localized URL path to canonical English.
+ ///
+ /// `/libro/abc123` → `/book/abc123`
+ /// `/livre/abc123` → `/book/abc123`
+ /// `/book/abc123` → `/book/abc123` (already canonical)
+ String canonicalizePath(String path) {
+ if (path.isEmpty || path == '/') return path;
+
+ final qIndex = path.indexOf('?');
+ final purePath = qIndex > -1 ? path.substring(0, qIndex) : path;
+ final query = qIndex > -1 ? path.substring(qIndex) : '';
+
+ final segments = purePath.split('/');
+ var changed = false;
+ for (var i = 0; i < segments.length; i++) {
+ final seg = segments[i];
+ if (seg.isEmpty) continue;
+ final canonical = _reverseMap[seg];
+ if (canonical != null) {
+ segments[i] = canonical;
+ changed = true;
+ }
+ }
+ if (!changed) return path;
+ return segments.join('/') + query;
+ }
+
+ /// Localizes a canonical English path to the given language code.
+ ///
+ /// `/book/abc123` → `/libro/abc123` (for `'es'`)
+ /// `/book/abc123` → `/book/abc123` (for `'en'`, no-op)
+ String localizePath(String path, String languageCode) {
+ if (path.isEmpty || path == '/') return path;
+ if (languageCode == 'en') return path;
+
+ final localMap = _forwardMaps[languageCode];
+ if (localMap == null) return path;
+
+ final qIndex = path.indexOf('?');
+ final purePath = qIndex > -1 ? path.substring(0, qIndex) : path;
+ final query = qIndex > -1 ? path.substring(qIndex) : '';
+
+ final segments = purePath.split('/');
+ var changed = false;
+ for (var i = 0; i < segments.length; i++) {
+ final seg = segments[i];
+ if (seg.isEmpty) continue;
+ final localized = localMap[seg];
+ if (localized != null) {
+ segments[i] = localized;
+ changed = true;
+ }
+ }
+ if (!changed) return path;
+ return segments.join('/') + query;
+ }
+
+ // ─── Diacritics ───────────────────────────────────────────────
+
+ static const _diacriticsFrom =
+ 'ÀÁÂÃÄÅàáâãäåÈÉÊËèéêëÌÍÎÏìíîïÒÓÔÕÖØòóôõöøÙÚÛÜùúûüÝýÿÑñÇç';
+ static const _diacriticsTo =
+ 'AAAAAAaaaaaaEEEEeeeeIIIIiiiiOOOOOOooooooUUUUuuuuYyyNnCc';
+
+ static final Map _charMap = () {
+ final map = {};
+ for (var i = 0; i < _diacriticsFrom.length; i++) {
+ map[_diacriticsFrom.codeUnitAt(i)] = _diacriticsTo[i];
+ }
+ return map;
+ }();
+
+ /// Replaces accented characters with their ASCII equivalents.
+ /// `Publicación` → `Publicacion`
+ static String _removeDiacritics(String input) {
+ final buffer = StringBuffer();
+ for (final unit in input.codeUnits) {
+ buffer.write(_charMap[unit] ?? String.fromCharCode(unit));
+ }
+ return buffer.toString();
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 221644b0..96b58760 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: sint
description: SINT (State, Injection, Navigation, Translation) - The Four Pillars of High-Fidelity Flutter Infrastructure.
-version: 1.1.0
+version: 1.2.1
homepage: https://github.com/Open-Neom/sint
environment: