@@ -78,18 +78,69 @@ extension ErrorExetension on Object {
7878 if (description.contains ('Cannot use your own code for promotion' )) {
7979 return "referral_code_own_invalid" .i18n;
8080 }
81- return description;
81+
82+ final categoryKey = _classifyVpnError (description);
83+ if (categoryKey != null ) return categoryKey.i18n;
84+
85+ return "an_error_occurred" .i18n;
8286 }
8387
8488 if (this is StateError ) {
85- return (this as StateError ).message;
89+ final categoryKey = _classifyVpnError ((this as StateError ).message);
90+ if (categoryKey != null ) return categoryKey.i18n;
91+ return "an_error_occurred" .i18n;
8692 }
8793 if (this is Exception ) {
88- return (this as Exception ).toString ();
94+ final categoryKey = _classifyVpnError ((this as Exception ).toString ());
95+ if (categoryKey != null ) return categoryKey.i18n;
96+ return "an_error_occurred" .i18n;
8997 }
9098
91- return "error_occurred" .i18n;
99+ return "an_error_occurred" .i18n;
100+ }
101+ }
102+
103+ /// Classifies VPN-related errors into user-friendly
104+ /// categories based on regex patterns.
105+ final List <(RegExp , String )> _vpnErrorPatterns = [
106+ (
107+ RegExp (
108+ r'no such host|dns|network is unreachable|i/o timeout|no route to host|connection refused' ,
109+ caseSensitive: false ,
110+ ),
111+ 'err_check_connection' ,
112+ ),
113+ (
114+ RegExp (r'\b503\b|service unavailable' , caseSensitive: false ),
115+ 'err_service_unavailable' ,
116+ ),
117+ (
118+ RegExp (r'ruleset|geosite|geoip|smart routing' , caseSensitive: false ),
119+ 'err_ruleset_failed' ,
120+ ),
121+ (
122+ RegExp (
123+ r'tunnel|tun device|setup failed|failed to start vpn|libbox' ,
124+ caseSensitive: false ,
125+ ),
126+ 'err_connection_failed' ,
127+ ),
128+ ];
129+
130+ String ? _classifyVpnError (String description) {
131+ if (description.isEmpty) return null ;
132+ for (final (pattern, key) in _vpnErrorPatterns) {
133+ if (pattern.hasMatch (description)) return key;
92134 }
135+ return null ;
136+ }
137+
138+ /// Returns a localized user-facing message for a raw error string. Use this
139+ /// at boundaries where errors arrive as plain strings (e.g. FFI results)
140+ /// rather than as `Exception` instances, instead of wrapping them in
141+ /// `Exception(...)` just to route through `localizedDescription` .
142+ String localizeRawError (String rawError) {
143+ return (_classifyVpnError (rawError) ?? 'an_error_occurred' ).i18n;
93144}
94145
95146/// Strips the radiance IPC prefix from error messages.
0 commit comments