mindroot 9.3.0__py3-none-any.whl → 9.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. mindroot/coreplugins/admin/__init__.py +3 -1
  2. mindroot/coreplugins/admin/agent_router.py +250 -7
  3. mindroot/coreplugins/admin/asset_manager.py +164 -0
  4. mindroot/coreplugins/admin/command_router.py +236 -1
  5. mindroot/coreplugins/admin/mcp_catalog_routes.py +156 -0
  6. mindroot/coreplugins/admin/mcp_publish_routes.py +450 -0
  7. mindroot/coreplugins/admin/mcp_registry_routes.py +495 -0
  8. mindroot/coreplugins/admin/mcp_routes.py +216 -0
  9. mindroot/coreplugins/admin/mod.py +62 -0
  10. mindroot/coreplugins/admin/oauth_callback_router.py +84 -0
  11. mindroot/coreplugins/admin/persona_handler.py +15 -6
  12. mindroot/coreplugins/admin/persona_router.py +158 -2
  13. mindroot/coreplugins/admin/plugin_manager.py +105 -9
  14. mindroot/coreplugins/admin/plugin_router_fixed.py +23 -0
  15. mindroot/coreplugins/admin/plugin_router_new_not_working.py +145 -0
  16. mindroot/coreplugins/admin/plugin_routes.py +114 -0
  17. mindroot/coreplugins/admin/registry_settings_routes.py +140 -0
  18. mindroot/coreplugins/admin/router.py +116 -15
  19. mindroot/coreplugins/admin/service_models.py +1 -1
  20. mindroot/coreplugins/admin/settings_router.py +1 -0
  21. mindroot/coreplugins/admin/static/css/admin-custom.css +357 -2
  22. mindroot/coreplugins/admin/static/css/dark.css +1 -0
  23. mindroot/coreplugins/admin/static/css/default.css +4 -0
  24. mindroot/coreplugins/admin/static/js/about-info.js +367 -0
  25. mindroot/coreplugins/admin/static/js/agent-form.js +83 -3
  26. mindroot/coreplugins/admin/static/js/api-key-script.js +307 -0
  27. mindroot/coreplugins/admin/static/js/mcp-manager.js +348 -0
  28. mindroot/coreplugins/admin/static/js/mcp-publisher.js +780 -0
  29. mindroot/coreplugins/admin/static/js/persona-editor.js +34 -5
  30. mindroot/coreplugins/admin/static/js/plugin-toggle.js +1 -1
  31. mindroot/coreplugins/admin/static/js/recommended-plugin-install.js +63 -0
  32. mindroot/coreplugins/admin/static/js/registry-auth-section.js +132 -0
  33. mindroot/coreplugins/admin/static/js/registry-manager-base.js +613 -0
  34. mindroot/coreplugins/admin/static/js/registry-manager-publish-old-delete.js +166 -0
  35. mindroot/coreplugins/admin/static/js/registry-manager.js +351 -0
  36. mindroot/coreplugins/admin/static/js/registry-publish-section.js +377 -0
  37. mindroot/coreplugins/admin/static/js/registry-search-section.js +400 -0
  38. mindroot/coreplugins/admin/static/js/registry-search-section.js.bak +3 -0
  39. mindroot/coreplugins/admin/static/js/registry-settings.js +69 -0
  40. mindroot/coreplugins/admin/static/js/registry-shared-services.js +903 -0
  41. mindroot/coreplugins/admin/static/js/registry-simple-sections.js +85 -0
  42. mindroot/coreplugins/admin/static/js/secure-widget-manager.js +438 -0
  43. mindroot/coreplugins/admin/static/logo.png +0 -0
  44. mindroot/coreplugins/admin/templates/admin.jinja2 +275 -110
  45. mindroot/coreplugins/agent/Assistant/agent.json +27 -11
  46. mindroot/coreplugins/agent/agent.py +2 -2
  47. mindroot/coreplugins/agent/command_parser.py +25 -10
  48. mindroot/coreplugins/agent/templates/system.jinja2 +0 -12
  49. mindroot/coreplugins/chat/__init__.py +4 -1
  50. mindroot/coreplugins/chat/router.py +132 -20
  51. mindroot/coreplugins/chat/router_dedup_patch.py +20 -0
  52. mindroot/coreplugins/chat/services.py +31 -1
  53. mindroot/coreplugins/chat/static/css/action-fix.css +32 -0
  54. mindroot/coreplugins/chat/static/css/admin-custom.css +5 -3
  55. mindroot/coreplugins/chat/static/css/dark.css +24 -3
  56. mindroot/coreplugins/chat/static/css/default.css +24 -3
  57. mindroot/coreplugins/chat/static/css/main.css +1 -0
  58. mindroot/coreplugins/chat/static/js/action.js +137 -60
  59. mindroot/coreplugins/chat/static/js/chat-history.js +3 -0
  60. mindroot/coreplugins/chat/static/js/chat.js +59 -16
  61. mindroot/coreplugins/chat/static/js/chat.js.diff +221 -0
  62. mindroot/coreplugins/chat/static/js/chatform.js +2 -2
  63. mindroot/coreplugins/chat/static/site.webmanifest +1 -1
  64. mindroot/coreplugins/chat/templates/chat.jinja2 +3 -3
  65. mindroot/coreplugins/chat/widget_manager.py +139 -0
  66. mindroot/coreplugins/chat/widget_routes.py +287 -0
  67. mindroot/coreplugins/check_list/inject/admin.jinja2 +1 -1
  68. mindroot/coreplugins/email/__init__.py +2 -0
  69. mindroot/coreplugins/email/email_provider.py +2 -2
  70. mindroot/coreplugins/email/mod.py +100 -0
  71. mindroot/coreplugins/email/services.py +5 -3
  72. mindroot/coreplugins/email/smtp_handler.py +9 -3
  73. mindroot/coreplugins/email/test_email_service.py +75 -0
  74. mindroot/coreplugins/env_manager/mod.py +61 -25
  75. mindroot/coreplugins/home/router.py +37 -2
  76. mindroot/coreplugins/home/static/imgs/logo.png +0 -0
  77. mindroot/coreplugins/home/static/imgs/logo.png.bak +0 -0
  78. mindroot/coreplugins/home/static/imgs/logo_teal.png +0 -0
  79. mindroot/coreplugins/home/static/imgs/logo_teal2.png +0 -0
  80. mindroot/coreplugins/home/static/imgs/logo_teal_detailed.png +0 -0
  81. mindroot/coreplugins/home/static/imgs/logo_teal_python.png +0 -0
  82. mindroot/coreplugins/home/templates/home.jinja2 +15 -6
  83. mindroot/coreplugins/index/indices/default/index.json +39 -6
  84. mindroot/coreplugins/jwt_auth/middleware.py +47 -2
  85. mindroot/coreplugins/jwt_auth/mod.py +40 -17
  86. mindroot/coreplugins/l8n/__init__.py +6 -0
  87. mindroot/coreplugins/l8n/debug_loader.py +85 -0
  88. mindroot/coreplugins/l8n/debug_middleware.py +74 -0
  89. mindroot/coreplugins/l8n/l8n_constants.py +19 -0
  90. mindroot/coreplugins/l8n/language_detection.py +183 -0
  91. mindroot/coreplugins/l8n/middleware.py +151 -0
  92. mindroot/coreplugins/l8n/mod.py +277 -0
  93. mindroot/coreplugins/l8n/monkey_patch_to_delete.py +186 -0
  94. mindroot/coreplugins/l8n/test_enhanced.py +298 -0
  95. mindroot/coreplugins/l8n/test_l8n.py +95 -0
  96. mindroot/coreplugins/l8n/test_l8n_standalone.py +251 -0
  97. mindroot/coreplugins/l8n/test_middleware.py +272 -0
  98. mindroot/coreplugins/l8n/utils.py +232 -0
  99. mindroot/coreplugins/mcp_/__init__.py +14 -0
  100. mindroot/coreplugins/mcp_/catalog_commands.py +328 -0
  101. mindroot/coreplugins/mcp_/catalog_manager.py +263 -0
  102. mindroot/coreplugins/mcp_/dynamic_commands.py +154 -0
  103. mindroot/coreplugins/mcp_/mcp_manager.py +1031 -0
  104. mindroot/coreplugins/mcp_/mod.py +367 -0
  105. mindroot/coreplugins/mcp_/oauth_storage.py +144 -0
  106. mindroot/coreplugins/mcp_/server_installer.py +79 -0
  107. mindroot/coreplugins/mcp_/setup.py +26 -0
  108. mindroot/coreplugins/mcp_/test_dynamic_commands.py +134 -0
  109. mindroot/coreplugins/mcp_/testmcpclient.py +92 -0
  110. mindroot/coreplugins/persona/mod.py +12 -7
  111. mindroot/coreplugins/signup/templates/signup.jinja2 +1 -1
  112. mindroot/coreplugins/subscriptions/__init__.py +1 -0
  113. mindroot/coreplugins/subscriptions/mod.py +14 -3
  114. mindroot/coreplugins/subscriptions/router.py +3 -0
  115. mindroot/coreplugins/user_service/__init__.py +1 -2
  116. mindroot/coreplugins/user_service/admin_init.py +1 -0
  117. mindroot/coreplugins/user_service/email_service.py +72 -17
  118. mindroot/coreplugins/user_service/mod.py +10 -2
  119. mindroot/coreplugins/user_service/router.py +2 -0
  120. mindroot/lib/auth/api_key.py +28 -0
  121. mindroot/lib/cli/plugins.py +94 -0
  122. mindroot/lib/plugins/default_plugin_manifest.json +20 -0
  123. mindroot/lib/plugins/installation.py +5 -5
  124. mindroot/lib/plugins/l8n_static_handler.py +225 -0
  125. mindroot/lib/plugins/loader.py +33 -3
  126. mindroot/lib/plugins/loader_with_l8n.py +281 -0
  127. mindroot/lib/plugins/manifest.py +236 -24
  128. mindroot/lib/providers/commands.py +3 -1
  129. mindroot/lib/route_decorators.py +5 -5
  130. mindroot/lib/templates.py +183 -11
  131. mindroot/lib/utils/merge_arrays.py +1 -1
  132. mindroot/migrate.py +39 -20
  133. mindroot/registry/data_access.py +1 -1
  134. mindroot/server.py +42 -13
  135. mindroot/server_missing_normal_args.py +197 -0
  136. mindroot/server_prev.py +173 -0
  137. {mindroot-9.3.0.dist-info → mindroot-9.6.0.dist-info}/METADATA +7 -2
  138. {mindroot-9.3.0.dist-info → mindroot-9.6.0.dist-info}/RECORD +143 -113
  139. mindroot/coreplugins/admin/plugin_manager_backup.py +0 -615
  140. mindroot/coreplugins/admin/static/favicon/about.txt +0 -6
  141. mindroot/coreplugins/admin/static/favicon/android-chrome-512x512.png +0 -0
  142. mindroot/coreplugins/admin/static/favicon/apple-touch-icon.png +0 -0
  143. mindroot/coreplugins/admin/static/favicon/favicon-16x16.png +0 -0
  144. mindroot/coreplugins/admin/static/favicon/favicon-32x32.png +0 -0
  145. mindroot/coreplugins/admin/static/favicon/favicon.ico +0 -0
  146. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/about.txt +0 -6
  147. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
  148. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
  149. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
  150. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
  151. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
  152. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/favicon.ico +0 -0
  153. mindroot/coreplugins/admin/static/favicon/favicon_io (1)/site.webmanifest +0 -1
  154. mindroot/coreplugins/admin/static/favicon/logo.png +0 -0
  155. mindroot/coreplugins/admin/static/favicon/site.webmanifest +0 -1
  156. mindroot/coreplugins/admin/static/js/backup/agent-editor.js +0 -186
  157. mindroot/coreplugins/admin/static/js/backup/agent-form.js +0 -1133
  158. mindroot/coreplugins/admin/static/js/backup/agent-list.js +0 -94
  159. mindroot/coreplugins/chat/static/favicon/about.txt +0 -6
  160. mindroot/coreplugins/chat/static/favicon/android-chrome-192x192.png +0 -0
  161. mindroot/coreplugins/chat/static/favicon/android-chrome-512x512.png +0 -0
  162. mindroot/coreplugins/chat/static/favicon/apple-touch-icon.png +0 -0
  163. mindroot/coreplugins/chat/static/favicon/favicon-16x16.png +0 -0
  164. mindroot/coreplugins/chat/static/favicon/favicon-32x32.png +0 -0
  165. mindroot/coreplugins/chat/static/favicon/favicon.ico +0 -0
  166. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/about.txt +0 -6
  167. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-192x192.png +0 -0
  168. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/android-chrome-512x512.png +0 -0
  169. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/apple-touch-icon.png +0 -0
  170. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-16x16.png +0 -0
  171. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon-32x32.png +0 -0
  172. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/favicon.ico +0 -0
  173. mindroot/coreplugins/chat/static/favicon/favicon_io (1)/site.webmanifest +0 -1
  174. mindroot/coreplugins/chat/static/favicon/logo.png +0 -0
  175. mindroot/coreplugins/chat/static/favicon/site.webmanifest +0 -1
  176. mindroot/coreplugins/index/default.json +0 -76
  177. mindroot/coreplugins/user_service/file_trigger_service.py +0 -12
  178. mindroot/coreplugins/user_service/hooks.py +0 -23
  179. /mindroot/coreplugins/{admin/static/favicon/android-chrome-192x192.png → home/static/imgs/backuplogo.png} +0 -0
  180. {mindroot-9.3.0.dist-info → mindroot-9.6.0.dist-info}/WHEEL +0 -0
  181. {mindroot-9.3.0.dist-info → mindroot-9.6.0.dist-info}/entry_points.txt +0 -0
  182. {mindroot-9.3.0.dist-info → mindroot-9.6.0.dist-info}/licenses/LICENSE +0 -0
  183. {mindroot-9.3.0.dist-info → mindroot-9.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script for the l8n middleware functionality.
4
+
5
+ This script simulates FastAPI request objects to test the middleware.
6
+ """
7
+
8
+ import asyncio
9
+ import os
10
+ from unittest.mock import Mock
11
+
12
+ # Mock FastAPI Request for testing
13
+ class MockRequest:
14
+ def __init__(self, path="/", query_params=None, cookies=None, headers=None):
15
+ self.url = Mock()
16
+ self.url.path = path
17
+ self.query_params = query_params or {}
18
+ self.cookies = cookies or {}
19
+ self.headers = headers or {}
20
+ self.state = Mock()
21
+
22
+ # Mock Response for testing
23
+ class MockResponse:
24
+ def __init__(self):
25
+ self.cookies_set = {}
26
+
27
+ def set_cookie(self, key, value, max_age=None, httponly=None, samesite=None):
28
+ self.cookies_set[key] = {
29
+ 'value': value,
30
+ 'max_age': max_age,
31
+ 'httponly': httponly,
32
+ 'samesite': samesite
33
+ }
34
+
35
+ # Import the middleware functions with sys.path manipulation
36
+ import sys
37
+ from pathlib import Path
38
+ sys.path.insert(0, str(Path(__file__).parent))
39
+
40
+ # Now import with absolute imports
41
+ import middleware
42
+ from language_detection import _parse_accept_language_header, get_fallback_language
43
+
44
+ # Create aliases for easier use
45
+ middleware_func = middleware.middleware
46
+ detect_language_from_request = middleware.detect_language_from_request
47
+ get_request_language = middleware.get_request_language
48
+
49
+ async def mock_call_next(request):
50
+ """Mock the next handler in the middleware chain."""
51
+ return MockResponse()
52
+
53
+ async def test_language_detection():
54
+ """Test language detection from various request sources."""
55
+ print("Testing Language Detection from Requests...\n")
56
+
57
+ # Test 1: URL parameter
58
+ print("1. Testing URL parameter detection...")
59
+ request = MockRequest(
60
+ path="/chat",
61
+ query_params={'lang': 'es'}
62
+ )
63
+ detected = detect_language_from_request(request)
64
+ print(f" URL param 'lang=es' -> {detected}")
65
+
66
+ # Test 2: Cookie value
67
+ print("\n2. Testing cookie detection...")
68
+ request = MockRequest(
69
+ path="/admin",
70
+ cookies={'mindroot_language': 'fr'}
71
+ )
72
+ detected = detect_language_from_request(request)
73
+ print(f" Cookie 'mindroot_language=fr' -> {detected}")
74
+
75
+ # Test 3: Accept-Language header
76
+ print("\n3. Testing Accept-Language header...")
77
+ test_headers = [
78
+ 'en-US,en;q=0.9,es;q=0.8',
79
+ 'es-ES,es;q=0.9,en;q=0.8',
80
+ 'fr-FR,fr;q=0.9',
81
+ 'de-DE,de;q=0.8,en;q=0.5',
82
+ 'zh-CN,zh;q=0.9,en;q=0.1'
83
+ ]
84
+
85
+ for header in test_headers:
86
+ request = MockRequest(
87
+ path="/test",
88
+ headers={'accept-language': header}
89
+ )
90
+ detected = detect_language_from_request(request)
91
+ print(f" '{header}' -> {detected}")
92
+
93
+ # Test 4: Environment variable
94
+ print("\n4. Testing environment variable...")
95
+ os.environ['MINDROOT_LANGUAGE'] = 'de'
96
+ request = MockRequest(path="/test")
97
+ detected = detect_language_from_request(request)
98
+ print(f" MINDROOT_LANGUAGE=de -> {detected}")
99
+
100
+ # Test 5: Priority order (URL param should override everything)
101
+ print("\n5. Testing priority order...")
102
+ request = MockRequest(
103
+ path="/test",
104
+ query_params={'lang': 'es'},
105
+ cookies={'mindroot_language': 'fr'},
106
+ headers={'accept-language': 'de-DE,de;q=0.9'}
107
+ )
108
+ detected = detect_language_from_request(request)
109
+ print(f" URL=es, Cookie=fr, Header=de -> {detected} (should be es)")
110
+
111
+ # Clean up
112
+ os.environ.pop('MINDROOT_LANGUAGE', None)
113
+
114
+ print("\n✅ Language detection tests completed!")
115
+
116
+ async def test_middleware_functionality():
117
+ """Test the complete middleware functionality."""
118
+ print("\nTesting Middleware Functionality...\n")
119
+
120
+ # Test 1: Basic middleware flow
121
+ print("1. Testing basic middleware flow...")
122
+ request = MockRequest(
123
+ path="/chat",
124
+ query_params={'lang': 'es'},
125
+ headers={'accept-language': 'en-US,en;q=0.9'}
126
+ )
127
+
128
+ response = await middleware_func(request, mock_call_next)
129
+
130
+ print(f" Request language set to: {request.state.language}")
131
+ print(f" Global language: {get_request_language()}")
132
+ print(f" Cookie set: {'mindroot_language' in response.cookies_set}")
133
+
134
+ if 'mindroot_language' in response.cookies_set:
135
+ cookie_info = response.cookies_set['mindroot_language']
136
+ print(f" Cookie value: {cookie_info['value']}")
137
+ print(f" Cookie max_age: {cookie_info['max_age']} seconds")
138
+
139
+ # Test 2: Different language sources
140
+ print("\n2. Testing different language sources...")
141
+
142
+ test_cases = [
143
+ {
144
+ 'name': 'Accept-Language only',
145
+ 'request': MockRequest(
146
+ path="/admin",
147
+ headers={'accept-language': 'fr-FR,fr;q=0.9,en;q=0.8'}
148
+ ),
149
+ 'expected': 'fr'
150
+ },
151
+ {
152
+ 'name': 'Cookie preference',
153
+ 'request': MockRequest(
154
+ path="/settings",
155
+ cookies={'mindroot_language': 'de'}
156
+ ),
157
+ 'expected': 'de'
158
+ },
159
+ {
160
+ 'name': 'URL override',
161
+ 'request': MockRequest(
162
+ path="/dashboard",
163
+ query_params={'lang': 'es'},
164
+ cookies={'mindroot_language': 'fr'}
165
+ ),
166
+ 'expected': 'es'
167
+ }
168
+ ]
169
+
170
+ for case in test_cases:
171
+ print(f"\n Testing: {case['name']}")
172
+ response = await middleware_func(case['request'], mock_call_next)
173
+ actual = case['request'].state.language
174
+ expected = case['expected']
175
+ status = "✅" if actual == expected else "❌"
176
+ print(f" {status} Expected: {expected}, Got: {actual}")
177
+
178
+ # Test 3: Error handling
179
+ print("\n3. Testing error handling...")
180
+
181
+ # Create a request that might cause issues
182
+ request = MockRequest(
183
+ path="/test",
184
+ headers={'accept-language': 'invalid-header-format'}
185
+ )
186
+
187
+ try:
188
+ response = await middleware_func(request, mock_call_next)
189
+ print(f" ✅ Error handled gracefully, language: {request.state.language}")
190
+ except Exception as e:
191
+ print(f" ❌ Unexpected error: {e}")
192
+
193
+ print("\n✅ Middleware functionality tests completed!")
194
+
195
+ async def test_integration_with_templates():
196
+ """Test integration with the template system."""
197
+ print("\nTesting Integration with Template System...\n")
198
+
199
+ # Import template-related functions
200
+ from test_l8n_standalone import set_translations, replace_placeholders
201
+
202
+ # Set up some translations
203
+ print("1. Setting up translations...")
204
+ await set_translations('en', {
205
+ 'page_title': 'Welcome to MindRoot',
206
+ 'nav_home': 'Home',
207
+ 'nav_settings': 'Settings'
208
+ })
209
+
210
+ await set_translations('es', {
211
+ 'page_title': 'Bienvenido a MindRoot',
212
+ 'nav_home': 'Inicio',
213
+ 'nav_settings': 'Configuración'
214
+ })
215
+
216
+ await set_translations('fr', {
217
+ 'page_title': 'Bienvenue à MindRoot',
218
+ 'nav_home': 'Accueil',
219
+ 'nav_settings': 'Paramètres'
220
+ })
221
+
222
+ # Template content
223
+ template = "<title>__TRANSLATE_page_title__</title><nav><a>__TRANSLATE_nav_home__</a><a>__TRANSLATE_nav_settings__</a></nav>"
224
+
225
+ # Test different language requests
226
+ print("\n2. Testing template translation with different requests...")
227
+
228
+ test_requests = [
229
+ ('English (default)', MockRequest(path="/")),
230
+ ('Spanish (URL param)', MockRequest(path="/", query_params={'lang': 'es'})),
231
+ ('French (cookie)', MockRequest(path="/", cookies={'mindroot_language': 'fr'})),
232
+ ('German (header)', MockRequest(path="/", headers={'accept-language': 'de-DE,de;q=0.9,en;q=0.8'}))
233
+ ]
234
+
235
+ for name, request in test_requests:
236
+ print(f"\n Testing: {name}")
237
+
238
+ # Process request through middleware
239
+ await middleware_func(request, mock_call_next)
240
+
241
+ # Get the detected language
242
+ detected_lang = get_request_language()
243
+ print(f" Detected language: {detected_lang}")
244
+
245
+ # Translate the template
246
+ translated = replace_placeholders(template, detected_lang)
247
+
248
+ # Extract title for display
249
+ title_start = translated.find('<title>') + 7
250
+ title_end = translated.find('</title>')
251
+ title = translated[title_start:title_end]
252
+
253
+ print(f" Translated title: {title}")
254
+
255
+ print("\n✅ Template integration tests completed!")
256
+
257
+ async def main():
258
+ """Run all middleware tests."""
259
+ print("=" * 60)
260
+ print("MindRoot l8n Plugin - Middleware Testing Suite")
261
+ print("=" * 60)
262
+
263
+ await test_language_detection()
264
+ await test_middleware_functionality()
265
+ await test_integration_with_templates()
266
+
267
+ print("\n" + "=" * 60)
268
+ print("✅ All middleware tests completed successfully!")
269
+ print("=" * 60)
270
+
271
+ if __name__ == "__main__":
272
+ asyncio.run(main())
@@ -0,0 +1,232 @@
1
+ # need to import Path
2
+ from pathlib import Path
3
+ from .l8n_constants import *
4
+ import json
5
+ import re
6
+ import logging
7
+
8
+ # Set up logging for l8n warnings
9
+ logger = logging.getLogger('l8n')
10
+ logger.setLevel(logging.WARNING)
11
+
12
+ # Create console handler with formatting
13
+ if not logger.handlers:
14
+ console_handler = logging.StreamHandler()
15
+ console_handler.setLevel(logging.WARNING)
16
+ formatter = logging.Formatter('\033[91m[L8N WARNING]\033[0m %(message)s')
17
+ console_handler.setFormatter(formatter)
18
+ logger.addHandler(console_handler)
19
+
20
+ def extract_plugin_root(absolute_path: str) -> str:
21
+ """
22
+ Extract the plugin root from an absolute path.
23
+
24
+ For core plugins: everything after 'coreplugins/'
25
+ For external plugins: everything after 'src/[plugin_name]/'
26
+
27
+ Examples:
28
+ - /files/mindroot/src/mindroot/coreplugins/chat/templates/chat.jinja2 -> chat/templates/chat.jinja2
29
+ - /some/path/src/my_plugin/templates/page.jinja2 -> my_plugin/templates/page.jinja2
30
+ """
31
+ path = Path(absolute_path)
32
+ parts = path.parts
33
+
34
+ # Find coreplugins in the path
35
+ if 'coreplugins' in parts:
36
+ coreplugins_idx = parts.index('coreplugins')
37
+ if coreplugins_idx + 1 < len(parts):
38
+ return '/'.join(parts[coreplugins_idx + 1:])
39
+
40
+ # Find src/[plugin_name] pattern for external plugins
41
+ for i, part in enumerate(parts):
42
+ if part == 'src' and i + 1 < len(parts):
43
+ # Check if this looks like a plugin (has templates, static, etc.)
44
+ potential_plugin = parts[i + 1]
45
+ if i + 2 < len(parts): # Has more path after src/plugin_name
46
+ return '/'.join(parts[i + 1:])
47
+
48
+ # Fallback: return the filename
49
+ return path.name
50
+
51
+ def extract_translation_keys(content: str) -> set:
52
+ """
53
+ Extract all __TRANSLATE_key__ placeholders from content.
54
+
55
+ Args:
56
+ content: Content to scan for translation keys
57
+
58
+ Returns:
59
+ Set of translation keys found in the content
60
+ """
61
+ pattern = r'__TRANSLATE_([a-z0-9_]+)__'
62
+ matches = re.findall(pattern, content)
63
+ return set(matches)
64
+
65
+ def replace_placeholders(content: str, language: str, plugin_path: str = None) -> str:
66
+ """
67
+ Replace __TRANSLATE_key__ placeholders with actual translations.
68
+
69
+ This function now validates that ALL required translations are available
70
+ for the specified language. If any translations are missing, it logs
71
+ a strong warning and returns None to indicate fallback is needed.
72
+
73
+ Args:
74
+ content: Template content with placeholders
75
+ language: Language code for translations
76
+ plugin_path: Path to the localized file (used to determine which plugin's translations to use)
77
+
78
+ Returns:
79
+ Content with placeholders replaced by translations, or None if translations are incomplete
80
+ """
81
+ if not plugin_path:
82
+ # No plugin path provided, return content unchanged
83
+ logger.warning("No plugin path provided for translation replacement.")
84
+ return content
85
+
86
+ try:
87
+ # Extract all translation keys from the content
88
+ required_keys = extract_translation_keys(content)
89
+
90
+ if not required_keys:
91
+ # No translation keys found, return content as-is
92
+ return content
93
+
94
+ # Extract plugin name from the localized file path
95
+ # Path format: .../localized_files/[coreplugins|external_plugins]/[plugin_name]/...
96
+ path_parts = Path(plugin_path).parts
97
+
98
+ # Find 'localized_files' in the path
99
+ if 'localized_files' in path_parts:
100
+ idx = path_parts.index('localized_files')
101
+ if idx + 2 < len(path_parts):
102
+ # Get plugin name (should be at idx+2)
103
+ plugin_type = path_parts[idx + 1] # 'coreplugins' or 'external_plugins'
104
+ plugin_name = path_parts[idx + 2]
105
+
106
+ # Load translations for this plugin
107
+ translations_path = TRANSLATIONS_DIR / plugin_type / plugin_name / "translations.json"
108
+ plugin_translations = {}
109
+ if translations_path.exists():
110
+ try:
111
+ with open(translations_path, 'r', encoding='utf-8') as f:
112
+ plugin_translations = json.load(f)
113
+ except Exception as e:
114
+ logger.warning(f"Could not load translations from {translations_path}: {e}")
115
+ return None # Fallback to original file
116
+
117
+ if language in plugin_translations:
118
+ translations = plugin_translations[language]
119
+
120
+ # Check if ALL required translations are available
121
+ missing_keys = required_keys - set(translations.keys())
122
+
123
+ if missing_keys:
124
+ # Some translations are missing - log strong warning and return None
125
+ missing_list = ', '.join(sorted(missing_keys))
126
+ logger.warning(
127
+ f"\n" +
128
+ f"="*80 + "\n" +
129
+ f"MISSING TRANSLATIONS DETECTED!\n" +
130
+ f"Plugin: {plugin_name}\n" +
131
+ f"Language: {language}\n" +
132
+ f"File: {plugin_path}\n" +
133
+ f"Missing keys: {missing_list}\n" +
134
+ f"Falling back to original file to avoid showing placeholders.\n" +
135
+ f"="*80
136
+ )
137
+ return None # Signal that fallback is needed
138
+
139
+ # All translations are available - proceed with replacement
140
+ def replace_match(match):
141
+ key = match.group(1)
142
+ return translations.get(key, match.group(0)) # This shouldn't happen now
143
+
144
+ return re.sub(r'__TRANSLATE_([a-z0-9_]+)__', replace_match, content)
145
+ else:
146
+ # No translations for this language
147
+ logger.warning(
148
+ f"\n" +
149
+ f"="*80 + "\n" +
150
+ f"NO TRANSLATIONS FOR LANGUAGE!\n" +
151
+ f"Plugin: {plugin_name}\n" +
152
+ f"Language: {language}\n" +
153
+ f"File: {plugin_path}\n" +
154
+ f"Required keys: {', '.join(sorted(required_keys))}\n" +
155
+ f"Falling back to original file.\n" +
156
+ f"="*80
157
+ )
158
+ return None # Signal that fallback is needed
159
+ else:
160
+ logger.warning(f"Could not extract plugin info from path: {plugin_path}")
161
+ return None
162
+ else:
163
+ logger.warning(f"Path does not contain 'localized_files': {plugin_path}")
164
+ return None
165
+
166
+ except Exception as e:
167
+ logger.warning(f"Error in replace_placeholders: {e}")
168
+ return None # Fallback to original file on any error
169
+
170
+ def get_localized_file_path(original_path: str) -> Path:
171
+ """
172
+ Convert an original file path to its localized version path.
173
+
174
+ Examples:
175
+ - chat/templates/chat.jinja2 -> localized_files/coreplugins/chat/templates/chat.i18n.jinja2
176
+ - my_plugin/templates/page.jinja2 -> localized_files/external_plugins/my_plugin/templates/page.i18n.jinja2
177
+ """
178
+ plugin_root = extract_plugin_root(original_path)
179
+ path = Path(plugin_root)
180
+
181
+ # Determine if this is a core plugin or external plugin based on the absolute path
182
+ if 'coreplugins' in original_path:
183
+ base_dir = LOCALIZED_FILES_DIR / "coreplugins"
184
+ else:
185
+ base_dir = LOCALIZED_FILES_DIR / "external_plugins"
186
+
187
+ # Add .i18n before the file extension
188
+ stem = path.stem
189
+ suffix = path.suffix
190
+ new_filename = f"{stem}.i18n{suffix}"
191
+
192
+ localized_path = base_dir / path.parent / new_filename
193
+ return localized_path
194
+
195
+
196
+ def load_plugin_translations(plugin_path: str):
197
+ """Load translations for a specific plugin from disk."""
198
+
199
+ translations_file = get_plugin_translations_path(plugin_path)
200
+
201
+ if translations_file.exists():
202
+ try:
203
+ with open(translations_file, 'r', encoding='utf-8') as f:
204
+ return json.load(f)
205
+ except Exception as e:
206
+ logger.warning(f"Could not load translations from {translations_file}: {e}")
207
+ else:
208
+ logger.warning(f"Translations file does not exist at {translations_file}")
209
+ return {}
210
+
211
+ def get_plugin_translations_path(original_path: str) -> Path:
212
+ """
213
+ Get the path where translations should be stored for a given file.
214
+
215
+ Example:
216
+ /files/mindroot/src/mindroot/coreplugins/check_list/inject/admin.jinja2
217
+ -> translations/coreplugins/check_list/translations.json
218
+ """
219
+ plugin_root = extract_plugin_root(original_path)
220
+
221
+ # Determine if this is a core plugin or external plugin based on the absolute path
222
+ if 'coreplugins' in original_path:
223
+ base_dir = TRANSLATIONS_DIR / "coreplugins"
224
+ else:
225
+ base_dir = TRANSLATIONS_DIR / "external_plugins"
226
+
227
+ # Get just the plugin name (first part of plugin_root)
228
+ plugin_name = Path(plugin_root).parts[0]
229
+
230
+ return base_dir / plugin_name / "translations.json"
231
+
232
+
@@ -0,0 +1,14 @@
1
+ # This import is required for the plugin to load properly
2
+ from .mod import *
3
+
4
+ # Import catalog features
5
+ try:
6
+ from .catalog_commands import *
7
+ except ImportError:
8
+ pass
9
+
10
+ # Import additional commands
11
+ try:
12
+ from .additional_commands import *
13
+ except ImportError:
14
+ pass