mindroot 9.3.0__py3-none-any.whl → 9.5.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 +63 -0
  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-old.js +385 -0
  35. mindroot/coreplugins/admin/static/js/registry-manager-publish-old-delete.js +166 -0
  36. mindroot/coreplugins/admin/static/js/registry-manager.js +351 -0
  37. mindroot/coreplugins/admin/static/js/registry-publish-section.js +377 -0
  38. mindroot/coreplugins/admin/static/js/registry-search-section.js +400 -0
  39. mindroot/coreplugins/admin/static/js/registry-search-section.js.bak +3 -0
  40. mindroot/coreplugins/admin/static/js/registry-settings.js +69 -0
  41. mindroot/coreplugins/admin/static/js/registry-shared-services.js +857 -0
  42. mindroot/coreplugins/admin/static/js/registry-simple-sections.js +85 -0
  43. mindroot/coreplugins/admin/static/js/secure-widget-manager.js +438 -0
  44. mindroot/coreplugins/admin/static/logo.png +0 -0
  45. mindroot/coreplugins/admin/templates/admin.jinja2 +275 -110
  46. mindroot/coreplugins/agent/Assistant/agent.json +27 -11
  47. mindroot/coreplugins/agent/agent.py +2 -2
  48. mindroot/coreplugins/agent/command_parser.py +25 -10
  49. mindroot/coreplugins/agent/templates/system.jinja2 +0 -12
  50. mindroot/coreplugins/chat/__init__.py +4 -1
  51. mindroot/coreplugins/chat/router.py +132 -20
  52. mindroot/coreplugins/chat/router_dedup_patch.py +20 -0
  53. mindroot/coreplugins/chat/services.py +31 -1
  54. mindroot/coreplugins/chat/static/css/action-fix.css +32 -0
  55. mindroot/coreplugins/chat/static/css/admin-custom.css +5 -3
  56. mindroot/coreplugins/chat/static/css/dark.css +24 -3
  57. mindroot/coreplugins/chat/static/css/default.css +24 -3
  58. mindroot/coreplugins/chat/static/css/main.css +1 -0
  59. mindroot/coreplugins/chat/static/js/action.js +137 -60
  60. mindroot/coreplugins/chat/static/js/chat-history.js +3 -0
  61. mindroot/coreplugins/chat/static/js/chat.js +59 -16
  62. mindroot/coreplugins/chat/static/js/chat.js.diff +221 -0
  63. mindroot/coreplugins/chat/static/js/chatform.js +2 -2
  64. mindroot/coreplugins/chat/static/site.webmanifest +1 -1
  65. mindroot/coreplugins/chat/templates/chat.jinja2 +3 -3
  66. mindroot/coreplugins/chat/widget_manager.py +139 -0
  67. mindroot/coreplugins/chat/widget_routes.py +287 -0
  68. mindroot/coreplugins/check_list/inject/admin.jinja2 +1 -1
  69. mindroot/coreplugins/email/__init__.py +2 -0
  70. mindroot/coreplugins/email/email_provider.py +2 -2
  71. mindroot/coreplugins/email/mod.py +100 -0
  72. mindroot/coreplugins/email/services.py +5 -3
  73. mindroot/coreplugins/email/smtp_handler.py +9 -3
  74. mindroot/coreplugins/email/test_email_service.py +75 -0
  75. mindroot/coreplugins/env_manager/mod.py +61 -25
  76. mindroot/coreplugins/home/router.py +37 -2
  77. mindroot/coreplugins/home/static/imgs/logo.png +0 -0
  78. mindroot/coreplugins/home/static/imgs/logo.png.bak +0 -0
  79. mindroot/coreplugins/home/static/imgs/logo_teal.png +0 -0
  80. mindroot/coreplugins/home/static/imgs/logo_teal2.png +0 -0
  81. mindroot/coreplugins/home/static/imgs/logo_teal_detailed.png +0 -0
  82. mindroot/coreplugins/home/static/imgs/logo_teal_python.png +0 -0
  83. mindroot/coreplugins/home/templates/home.jinja2 +15 -6
  84. mindroot/coreplugins/index/indices/default/index.json +6 -6
  85. mindroot/coreplugins/jwt_auth/middleware.py +47 -2
  86. mindroot/coreplugins/jwt_auth/mod.py +40 -17
  87. mindroot/coreplugins/l8n/__init__.py +6 -0
  88. mindroot/coreplugins/l8n/debug_loader.py +85 -0
  89. mindroot/coreplugins/l8n/debug_middleware.py +74 -0
  90. mindroot/coreplugins/l8n/l8n_constants.py +19 -0
  91. mindroot/coreplugins/l8n/language_detection.py +183 -0
  92. mindroot/coreplugins/l8n/middleware.py +151 -0
  93. mindroot/coreplugins/l8n/mod.py +277 -0
  94. mindroot/coreplugins/l8n/monkey_patch_to_delete.py +186 -0
  95. mindroot/coreplugins/l8n/test_enhanced.py +298 -0
  96. mindroot/coreplugins/l8n/test_l8n.py +95 -0
  97. mindroot/coreplugins/l8n/test_l8n_standalone.py +251 -0
  98. mindroot/coreplugins/l8n/test_middleware.py +272 -0
  99. mindroot/coreplugins/l8n/utils.py +232 -0
  100. mindroot/coreplugins/mcp_/__init__.py +14 -0
  101. mindroot/coreplugins/mcp_/catalog_commands.py +328 -0
  102. mindroot/coreplugins/mcp_/catalog_manager.py +263 -0
  103. mindroot/coreplugins/mcp_/dynamic_commands.py +154 -0
  104. mindroot/coreplugins/mcp_/mcp_manager.py +1031 -0
  105. mindroot/coreplugins/mcp_/mod.py +367 -0
  106. mindroot/coreplugins/mcp_/oauth_storage.py +144 -0
  107. mindroot/coreplugins/mcp_/server_installer.py +79 -0
  108. mindroot/coreplugins/mcp_/setup.py +26 -0
  109. mindroot/coreplugins/mcp_/test_dynamic_commands.py +134 -0
  110. mindroot/coreplugins/mcp_/testmcpclient.py +92 -0
  111. mindroot/coreplugins/persona/mod.py +12 -7
  112. mindroot/coreplugins/signup/templates/signup.jinja2 +1 -1
  113. mindroot/coreplugins/subscriptions/__init__.py +1 -0
  114. mindroot/coreplugins/subscriptions/mod.py +14 -3
  115. mindroot/coreplugins/subscriptions/router.py +3 -0
  116. mindroot/coreplugins/user_service/__init__.py +1 -2
  117. mindroot/coreplugins/user_service/admin_init.py +1 -0
  118. mindroot/coreplugins/user_service/email_service.py +72 -17
  119. mindroot/coreplugins/user_service/mod.py +10 -2
  120. mindroot/coreplugins/user_service/router.py +2 -0
  121. mindroot/lib/auth/api_key.py +28 -0
  122. mindroot/lib/cli/plugins.py +94 -0
  123. mindroot/lib/plugins/default_plugin_manifest.json +20 -0
  124. mindroot/lib/plugins/installation.py +5 -5
  125. mindroot/lib/plugins/l8n_static_handler.py +225 -0
  126. mindroot/lib/plugins/loader.py +33 -3
  127. mindroot/lib/plugins/loader_with_l8n.py +281 -0
  128. mindroot/lib/plugins/manifest.py +236 -24
  129. mindroot/lib/providers/commands.py +3 -1
  130. mindroot/lib/route_decorators.py +5 -5
  131. mindroot/lib/templates.py +183 -11
  132. mindroot/lib/utils/merge_arrays.py +1 -1
  133. mindroot/migrate.py +39 -20
  134. mindroot/registry/data_access.py +1 -1
  135. mindroot/server.py +42 -13
  136. mindroot/server_missing_normal_args.py +197 -0
  137. mindroot/server_prev.py +173 -0
  138. {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/METADATA +7 -2
  139. {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/RECORD +144 -112
  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.5.0.dist-info}/WHEEL +0 -0
  181. {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/entry_points.txt +0 -0
  182. {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/licenses/LICENSE +0 -0
  183. {mindroot-9.3.0.dist-info → mindroot-9.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Debug script to test middleware import and functionality.
4
+ """
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ # Add the mindroot path
10
+ sys.path.insert(0, '/files/mindroot/src/mindroot')
11
+
12
+ try:
13
+ print("Testing middleware import...")
14
+ import coreplugins.l8n.middleware as mw
15
+ print(f"✅ Middleware imported successfully")
16
+ print(f"✅ Has middleware function: {hasattr(mw, 'middleware')}")
17
+ print(f"✅ Middleware type: {type(mw.middleware)}")
18
+
19
+ # Test if the function is callable
20
+ import inspect
21
+ print(f"✅ Function signature: {inspect.signature(mw.middleware)}")
22
+ print(f"✅ Is coroutine function: {inspect.iscoroutinefunction(mw.middleware)}")
23
+
24
+ # Test basic functionality
25
+ print("\nTesting basic middleware functionality...")
26
+
27
+ # Mock request and call_next
28
+ class MockRequest:
29
+ def __init__(self):
30
+ self.url = type('obj', (object,), {'path': '/test'})
31
+ self.query_params = {}
32
+ self.cookies = {}
33
+ self.headers = {}
34
+ self.state = type('obj', (object,), {})
35
+
36
+ class MockResponse:
37
+ def __init__(self):
38
+ self.cookies_set = {}
39
+ def set_cookie(self, **kwargs):
40
+ self.cookies_set.update(kwargs)
41
+
42
+ async def mock_call_next(request):
43
+ return MockResponse()
44
+
45
+ # Test the middleware
46
+ import asyncio
47
+
48
+ async def test_middleware():
49
+ request = MockRequest()
50
+ try:
51
+ response = await mw.middleware(request, mock_call_next)
52
+ print(f"✅ Middleware executed successfully")
53
+ print(f"✅ Request language set: {hasattr(request.state, 'language')}")
54
+ if hasattr(request.state, 'language'):
55
+ print(f"✅ Language: {request.state.language}")
56
+ return True
57
+ except Exception as e:
58
+ print(f"❌ Middleware execution failed: {e}")
59
+ import traceback
60
+ traceback.print_exc()
61
+ return False
62
+
63
+ # Run the test
64
+ result = asyncio.run(test_middleware())
65
+
66
+ if result:
67
+ print("\n🎉 All middleware tests passed!")
68
+ else:
69
+ print("\n💥 Middleware tests failed!")
70
+
71
+ except Exception as e:
72
+ print(f"❌ Error during testing: {e}")
73
+ import traceback
74
+ traceback.print_exc()
@@ -0,0 +1,19 @@
1
+ from pathlib import Path
2
+ import os
3
+
4
+ L8N_AVAILABLE = True
5
+
6
+ # Base directory for l8n plugin
7
+ L8N_DIR = Path(os.environ.get('MR_SOURCE_DIR', '/files/mindroot/src/mindroot/coreplugins/l8n'))
8
+
9
+ # Base directory for localized files
10
+ LOCALIZED_FILES_DIR = L8N_DIR / "localized_files"
11
+
12
+ # Base directory for translations
13
+ TRANSLATIONS_DIR = L8N_DIR / "translations"
14
+
15
+ # Global cache for translations
16
+ # Structure: {plugin_path: {language: {key: translation}}}
17
+ TRANSLATIONS = {}
18
+
19
+
@@ -0,0 +1,183 @@
1
+ import os
2
+ from typing import Optional
3
+
4
+ def get_current_language_from_request() -> str:
5
+ """
6
+ Get the current language from various request sources.
7
+
8
+ This function checks multiple sources in order of priority:
9
+ 1. Environment variable MINDROOT_LANGUAGE
10
+ 2. FastAPI request context (if available)
11
+ 3. HTTP Accept-Language header
12
+ 4. URL parameters (?lang=es)
13
+ 5. Cookie values
14
+ 6. User preferences from database/session
15
+
16
+ Returns:
17
+ Language code (e.g., 'en', 'es', 'fr')
18
+ """
19
+ # 1. Check environment variable first (for testing/override)
20
+ env_lang = os.environ.get('MINDROOT_LANGUAGE')
21
+ if env_lang:
22
+ return env_lang
23
+
24
+ # 2. Try to get language from FastAPI request context
25
+ try:
26
+ from contextvars import ContextVar
27
+ import contextvars
28
+
29
+ # Try to find request context
30
+ request_lang = _get_language_from_fastapi_context()
31
+ if request_lang:
32
+ return request_lang
33
+ except (ImportError, Exception):
34
+ pass
35
+
36
+ # 3. Fallback to default language
37
+ return 'en'
38
+
39
+ def _get_language_from_fastapi_context() -> Optional[str]:
40
+ """
41
+ Extract language from FastAPI request context.
42
+
43
+ This function attempts to access the current FastAPI request
44
+ and extract language information from various sources.
45
+
46
+ Returns:
47
+ Language code if found, None otherwise
48
+ """
49
+ try:
50
+ # Try to get the current request from FastAPI context
51
+ from starlette.requests import Request
52
+ import contextvars
53
+
54
+ # This is a placeholder - in a real implementation, we would need
55
+ # to access the actual request context that MindRoot uses
56
+ # For now, we'll implement the detection logic structure
57
+
58
+ # Check if there's a way to get the current request
59
+ # This would need to be integrated with MindRoot's request handling
60
+
61
+ return None # Placeholder
62
+
63
+ except (ImportError, Exception):
64
+ return None
65
+
66
+ def _parse_accept_language_header(accept_language: str) -> str:
67
+ """
68
+ Parse the Accept-Language header and return the preferred language.
69
+
70
+ Args:
71
+ accept_language: Accept-Language header value
72
+
73
+ Returns:
74
+ Preferred language code
75
+
76
+ Example:
77
+ 'en-US,en;q=0.9,es;q=0.8,fr;q=0.7' -> 'en'
78
+ """
79
+ if not accept_language:
80
+ return 'en'
81
+
82
+ # Parse the Accept-Language header
83
+ languages = []
84
+ for lang_range in accept_language.split(','):
85
+ lang_range = lang_range.strip()
86
+ if ';q=' in lang_range:
87
+ lang, quality = lang_range.split(';q=', 1)
88
+ try:
89
+ quality = float(quality)
90
+ except ValueError:
91
+ quality = 1.0
92
+ else:
93
+ lang = lang_range
94
+ quality = 1.0
95
+
96
+ # Extract just the language code (e.g., 'en' from 'en-US')
97
+ lang_code = lang.split('-')[0].lower()
98
+ languages.append((lang_code, quality))
99
+
100
+ # Sort by quality (highest first)
101
+ languages.sort(key=lambda x: x[1], reverse=True)
102
+
103
+ # Return the highest quality language
104
+ if languages:
105
+ return languages[0][0]
106
+
107
+ return 'en'
108
+
109
+ def set_language_for_request(language: str):
110
+ """
111
+ Set the language for the current request context.
112
+
113
+ This function would be called by middleware or route handlers
114
+ to set the language for the current request.
115
+
116
+ Args:
117
+ language: Language code to set
118
+ """
119
+ # Set environment variable as a simple implementation
120
+ os.environ['MINDROOT_LANGUAGE'] = language
121
+
122
+ # In a real implementation, this would set the language
123
+ # in the request context or thread-local storage
124
+
125
+ def get_supported_languages() -> list:
126
+ """
127
+ Get the list of supported languages.
128
+
129
+ Returns:
130
+ List of supported language codes
131
+ """
132
+ # This could be configured via environment variables or config files
133
+ supported = os.environ.get('MINDROOT_SUPPORTED_LANGUAGES', 'en,es,fr,de,it,pt,ru,zh,ja,ko')
134
+ return [lang.strip() for lang in supported.split(',')]
135
+
136
+ def is_language_supported(language: str) -> bool:
137
+ """
138
+ Check if a language is supported.
139
+
140
+ Args:
141
+ language: Language code to check
142
+
143
+ Returns:
144
+ True if language is supported, False otherwise
145
+ """
146
+ return language in get_supported_languages()
147
+
148
+ def get_fallback_language(language: str) -> str:
149
+ """
150
+ Get a fallback language if the requested language is not supported.
151
+
152
+ Args:
153
+ language: Requested language code
154
+
155
+ Returns:
156
+ Fallback language code
157
+ """
158
+ if is_language_supported(language):
159
+ return language
160
+
161
+ # Language family fallbacks
162
+ fallbacks = {
163
+ 'en-us': 'en',
164
+ 'en-gb': 'en',
165
+ 'es-es': 'es',
166
+ 'es-mx': 'es',
167
+ 'fr-fr': 'fr',
168
+ 'fr-ca': 'fr',
169
+ 'de-de': 'de',
170
+ 'de-at': 'de',
171
+ 'pt-br': 'pt',
172
+ 'pt-pt': 'pt',
173
+ 'zh-cn': 'zh',
174
+ 'zh-tw': 'zh',
175
+ }
176
+
177
+ # Try fallback
178
+ fallback = fallbacks.get(language.lower())
179
+ if fallback and is_language_supported(fallback):
180
+ return fallback
181
+
182
+ # Default to English
183
+ return 'en'
@@ -0,0 +1,151 @@
1
+ from fastapi import Request
2
+
3
+ try:
4
+ from .language_detection import (
5
+ _parse_accept_language_header,
6
+ get_fallback_language,
7
+ set_language_for_request
8
+ )
9
+ except ImportError:
10
+ # For standalone testing
11
+ from language_detection import (
12
+ _parse_accept_language_header,
13
+ get_fallback_language,
14
+ set_language_for_request
15
+ )
16
+ import os
17
+
18
+ # Global variable to store the current request language
19
+ # This will be set by the middleware for each request
20
+ _current_request_language = None
21
+
22
+ def get_request_language() -> str:
23
+ """
24
+ Get the language that was detected for the current request.
25
+
26
+ This function is called by the monkey patch system to get the
27
+ language for template translation.
28
+
29
+ Returns:
30
+ Language code for the current request
31
+ """
32
+ global _current_request_language
33
+ print(f"L8n: Current request language is '{_current_request_language}'")
34
+ # If we have a request-specific language, use it
35
+ if _current_request_language:
36
+ return _current_request_language
37
+
38
+ # Fallback to environment variable or default
39
+ print("L8n: No request language set, falling back to environment variable or default")
40
+ return os.environ.get('MINDROOT_LANGUAGE', 'en')
41
+
42
+ def detect_language_from_request(request: Request) -> str:
43
+ """
44
+ Detect the preferred language from a FastAPI request.
45
+
46
+ Checks multiple sources in order of priority:
47
+ 1. URL parameter (?lang=es)
48
+ 2. Cookie value (mindroot_language)
49
+ 3. Accept-Language header
50
+ 4. Environment variable
51
+ 5. Default to 'en'
52
+
53
+ Args:
54
+ request: FastAPI Request object
55
+
56
+ Returns:
57
+ Detected language code
58
+ """
59
+ # 1. Check URL parameter first (highest priority)
60
+ print(f"L8n: Checking URL parameters for language: {request.query_params}")
61
+ url_lang = request.query_params.get('lang')
62
+ if url_lang:
63
+
64
+ return get_fallback_language(url_lang.lower())
65
+
66
+ # 2. Check cookie value
67
+ print(f"L8n: Checking cookies for language: {request.cookies}")
68
+ cookie_lang = request.cookies.get('mindroot_language')
69
+ if cookie_lang:
70
+ print(f"L8n: Found language in cookie: {cookie_lang}")
71
+ return get_fallback_language(cookie_lang.lower())
72
+
73
+ # 3. Check Accept-Language header
74
+ print(f"L8n: Accept-Language header: {request.headers.get('accept-language')}")
75
+ accept_language = request.headers.get('accept-language')
76
+ if accept_language:
77
+ print(f"L8n: Parsing Accept-Language header: {accept_language}")
78
+ parsed_lang = _parse_accept_language_header(accept_language)
79
+ if parsed_lang:
80
+ print(f"L8n: Parsed language from header: {parsed_lang}")
81
+ return get_fallback_language(parsed_lang)
82
+
83
+ # 4. Check environment variable
84
+ env_lang = os.environ.get('MINDROOT_LANGUAGE')
85
+ if env_lang:
86
+ return get_fallback_language(env_lang.lower())
87
+
88
+ # 5. Default to English
89
+ return 'en'
90
+
91
+ async def middleware(request: Request, call_next):
92
+ """
93
+ L8n middleware for language detection and setting.
94
+
95
+ This middleware runs early in the request pipeline to:
96
+ 1. Detect the preferred language for the request
97
+ 2. Store it globally for use by the template system
98
+ 3. Set it in the request state for other components
99
+
100
+ Args:
101
+ request: FastAPI Request object
102
+ call_next: Next middleware/handler in the chain
103
+
104
+ Returns:
105
+ Response from the next handler
106
+ """
107
+ global _current_request_language
108
+ try:
109
+ print("L8n middleware: Starting language detection")
110
+ # Detect the language for this request
111
+ detected_language = detect_language_from_request(request)
112
+
113
+ print(f"L8n: Detected language '{detected_language}' for {request.url.path}")
114
+ # Store it globally for the template system
115
+ _current_request_language = detected_language
116
+
117
+ # Also store it in request state for other components
118
+ request.state.language = detected_language
119
+
120
+ # Set it using the language detection system
121
+ set_language_for_request(detected_language)
122
+
123
+ # Debug logging (can be removed in production)
124
+ print(f"L8n: Detected language '{detected_language}' for {request.url.path}")
125
+
126
+ # Process the request
127
+ response = await call_next(request)
128
+
129
+ # Optionally set a cookie to remember the language preference
130
+ # Only set if it was explicitly requested via URL parameter
131
+ if request.query_params.get('lang'):
132
+ response.set_cookie(
133
+ key="mindroot_language",
134
+ value=detected_language,
135
+ max_age=30 * 24 * 60 * 60, # 30 days
136
+ httponly=True,
137
+ samesite="lax"
138
+ )
139
+
140
+ return response
141
+
142
+ except Exception as e:
143
+ print(f"L8n middleware error: {e}")
144
+ # If there's an error, continue with default language
145
+ _current_request_language = 'en'
146
+ request.state.language = 'en'
147
+ return await call_next(request)
148
+
149
+ finally:
150
+ # Clean up the global variable after the request
151
+ _current_request_language = None
@@ -0,0 +1,277 @@
1
+ import os
2
+ import re
3
+ import json
4
+ from pathlib import Path
5
+
6
+ # Try to import from lib first (for same instance), fallback to mindroot.lib
7
+ try:
8
+ from lib.providers.commands import command, command_manager
9
+ print("l8n: Using lib.providers.commands (same instance)")
10
+ except ImportError:
11
+ from mindroot.lib.providers.commands import command, command_manager
12
+ print("l8n: Using mindroot.lib.providers.commands (fallback)")
13
+
14
+ from .utils import extract_plugin_root, get_localized_file_path, load_plugin_translations, get_plugin_translations_path
15
+ from mindroot.lib.utils.debug import debug_box
16
+
17
+ debug_box("l8n: Top of mod.py")
18
+
19
+ from .l8n_constants import *
20
+
21
+ def save_plugin_translations(plugin_path: str, translations: dict):
22
+ """Save translations for a specific plugin to disk."""
23
+ translations_file = get_plugin_translations_path(plugin_path)
24
+
25
+ try:
26
+ # Ensure directory exists
27
+ translations_file.parent.mkdir(parents=True, exist_ok=True)
28
+ with open(translations_file, 'w', encoding='utf-8') as f:
29
+ json.dump(translations, f, indent=2, ensure_ascii=False)
30
+ return True
31
+ except Exception as e:
32
+ print(f"Warning: Could not save translations to {translations_file}: {e}")
33
+ return False
34
+
35
+
36
+ debug_box("l8n: defining command")
37
+
38
+ # Debug: Check if command_manager has functions
39
+ debug_box(f"l8n: command_manager has {len(command_manager.functions)} functions before registration")
40
+ debug_box(f"l8n: command_manager instance ID: {id(command_manager)}")
41
+
42
+ @command()
43
+ async def write_localized_file(original_path: str, content: str, context=None):
44
+ """
45
+ Write a localized version of a file with static placeholders.
46
+
47
+ This command creates a localized version of a template or source file
48
+ with __TRANSLATE_key__ placeholders that will be replaced with actual
49
+ translations when the file is loaded.
50
+
51
+ PLACEHOLDER FORMAT RULES:
52
+ - Always use the exact format: __TRANSLATE_key_name__
53
+ - Must start with __TRANSLATE_ and end with __
54
+ - Use lowercase letters, numbers, and underscores only for the key name
55
+ - Use descriptive, hierarchical key names like section_element or buttons_save
56
+ - NO spaces, hyphens, or special characters in key names
57
+
58
+ Args:
59
+ original_path: Absolute path to the original file
60
+ content: File content with __TRANSLATE_key__ placeholders
61
+ context: Command context (optional)
62
+
63
+ Examples:
64
+ await write_localized_file(
65
+ "/files/mindroot/src/mindroot/coreplugins/chat/templates/chat.jinja2",
66
+ "<h1>__TRANSLATE_chat_title__</h1><button>__TRANSLATE_buttons_send__</button>"
67
+ )
68
+
69
+ await write_localized_file(
70
+ "/some/path/src/my_plugin/templates/dashboard.jinja2",
71
+ "<div>__TRANSLATE_dashboard_welcome__</div>"
72
+ )
73
+ """
74
+ try:
75
+ localized_path = get_localized_file_path(original_path)
76
+
77
+ # Create directory if it doesn't exist
78
+ localized_path.parent.mkdir(parents=True, exist_ok=True)
79
+
80
+ # Write the content
81
+ with open(localized_path, 'w', encoding='utf-8') as f:
82
+ f.write(content)
83
+
84
+ return f"Localized file written to: {localized_path}"
85
+
86
+ except Exception as e:
87
+ return f"Error writing localized file: {str(e)}"
88
+
89
+ @command()
90
+ async def append_localized_file(original_path: str, content: str, context=None):
91
+ """
92
+ Append content to an existing localized file.
93
+
94
+ Use this for large files that need to be built incrementally.
95
+ Follow the same placeholder format rules as write_localized_file.
96
+
97
+ Args:
98
+ original_path: Absolute path to the original file
99
+ content: Content to append with __TRANSLATE_key__ placeholders
100
+ context: Command context (optional)
101
+
102
+ Example:
103
+ # First write the beginning
104
+ await write_localized_file(
105
+ "/path/to/large_template.jinja2",
106
+ "<html><head><title>__TRANSLATE_page_title__</title></head>"
107
+ )
108
+
109
+ # Then append more sections
110
+ await append_localized_file(
111
+ "/path/to/large_template.jinja2",
112
+ "<body><h1>__TRANSLATE_main_heading__</h1></body></html>"
113
+ )
114
+ """
115
+ try:
116
+ localized_path = get_localized_file_path(original_path)
117
+
118
+ # Create directory if it doesn't exist
119
+ localized_path.parent.mkdir(parents=True, exist_ok=True)
120
+
121
+ # Append the content
122
+ with open(localized_path, 'a', encoding='utf-8') as f:
123
+ f.write(content)
124
+
125
+ return f"Content appended to: {localized_path}"
126
+
127
+ except Exception as e:
128
+ return f"Error appending to localized file: {str(e)}"
129
+
130
+ @command()
131
+ async def set_translations(original_path: str, language: str, translations: dict, context=None):
132
+ """
133
+ Set translations for a specific language and plugin.
134
+
135
+ This command stores the translation mappings that will be used to replace
136
+ __TRANSLATE_key__ placeholders in localized files. Translations are stored
137
+ per plugin based on the provided file path.
138
+
139
+ Args:
140
+ original_path: Absolute path to a file in the plugin (used to identify the plugin)
141
+ language: Language code (e.g., 'en', 'es', 'fr', 'de')
142
+ translations: Dictionary mapping translation keys to translated text
143
+ context: Command context (optional)
144
+
145
+ Examples:
146
+ await set_translations(
147
+ "/files/mindroot/src/mindroot/coreplugins/chat/templates/chat.jinja2",
148
+ 'es',
149
+ {
150
+ 'chat_title': 'Interfaz de Chat',
151
+ 'buttons_send': 'Enviar Mensaje',
152
+ 'nav_home': 'Inicio',
153
+ 'error_connection_failed': 'Error de conexión'
154
+ }
155
+ )
156
+
157
+ await set_translations(
158
+ "/some/path/src/my_plugin/templates/dashboard.jinja2",
159
+ 'fr',
160
+ {
161
+ 'dashboard_welcome': 'Bienvenue au Tableau de Bord',
162
+ 'buttons_save': 'Enregistrer',
163
+ 'nav_home': 'Accueil'
164
+ }
165
+ )
166
+ """
167
+ try:
168
+ if not isinstance(translations, dict):
169
+ return "Error: translations must be a dictionary"
170
+
171
+ # Get the plugin-specific translations path
172
+ plugin_key = str(get_plugin_translations_path(original_path))
173
+
174
+ # Load existing translations for this plugin
175
+ plugin_translations = load_plugin_translations(original_path)
176
+
177
+ # Validate translation keys (should match placeholder format)
178
+ invalid_keys = []
179
+ for key in translations.keys():
180
+ if not re.match(r'^[a-z0-9_]+$', key):
181
+ invalid_keys.append(key)
182
+
183
+ if invalid_keys:
184
+ return f"Error: Invalid translation keys (use lowercase, numbers, underscores only): {invalid_keys}"
185
+
186
+ # Update translations for this language
187
+ if language not in plugin_translations:
188
+ plugin_translations[language] = {}
189
+
190
+ plugin_translations[language].update(translations)
191
+
192
+ # Save translations to disk
193
+ if save_plugin_translations(original_path, plugin_translations):
194
+ # Update cache
195
+ TRANSLATIONS[plugin_key] = plugin_translations
196
+ return f"Set {len(translations)} translations for language '{language}' in {Path(plugin_key).parent.name} plugin"
197
+ else:
198
+ return f"Error: Could not save translations"
199
+
200
+ except Exception as e:
201
+ return f"Error setting translations: {str(e)}"
202
+
203
+ @command()
204
+ async def get_translations(original_path: str = None, language: str = None, context=None):
205
+ """
206
+ Get translations for a specific plugin and language.
207
+
208
+ Args:
209
+ original_path: Absolute path to a file in the plugin (optional)
210
+ If not provided, returns all cached translations
211
+ language: Language code to get translations for (optional)
212
+ context: Command context (optional)
213
+
214
+ Returns:
215
+ Dictionary of translations
216
+
217
+ Examples:
218
+ # Get all translations for a plugin
219
+ translations = await get_translations(
220
+ "/files/mindroot/src/mindroot/coreplugins/chat/templates/chat.jinja2"
221
+ )
222
+
223
+ # Get Spanish translations for a plugin
224
+ spanish = await get_translations(
225
+ "/files/mindroot/src/mindroot/coreplugins/chat/templates/chat.jinja2",
226
+ 'es'
227
+ )
228
+
229
+ # Get all cached translations
230
+ all_cached = await get_translations()
231
+ """
232
+ try:
233
+ if original_path:
234
+ # Load translations for specific plugin
235
+ plugin_translations = load_plugin_translations(original_path)
236
+
237
+ # Update cache
238
+ plugin_key = str(get_plugin_translations_path(original_path))
239
+ TRANSLATIONS[plugin_key] = plugin_translations
240
+
241
+ if language:
242
+ return plugin_translations.get(language, {})
243
+ else:
244
+ return plugin_translations
245
+ else:
246
+ # Return all cached translations
247
+ return TRANSLATIONS
248
+
249
+ except Exception as e:
250
+ return f"Error getting translations: {str(e)}"
251
+
252
+ @command()
253
+ async def list_localized_files(context=None):
254
+ """
255
+ List all localized files that have been created.
256
+
257
+ Returns:
258
+ List of paths to localized files
259
+ """
260
+ try:
261
+ localized_files = []
262
+
263
+ if LOCALIZED_FILES_DIR.exists():
264
+ for file_path in LOCALIZED_FILES_DIR.rglob("*.i18n.*"):
265
+ localized_files.append(str(file_path.relative_to(LOCALIZED_FILES_DIR)))
266
+
267
+ return {
268
+ "count": len(localized_files),
269
+ "files": sorted(localized_files)
270
+ }
271
+
272
+ except Exception as e:
273
+ return f"Error listing localized files: {str(e)}"
274
+
275
+ debug_box(f"l8n: command_manager has {len(command_manager.functions)} functions after registration")
276
+
277
+ debug_box("l8n: End of mod.py")