Repository navigation
fix: move i18n routes from RestServer to service-i18n self-registration + dispatcher bridge - #911
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…Plugin - Remove `enableI18n` from RestApiConfigSchema (rest-server.zod.ts) - Remove `registerI18nEndpoints()` method from RestServer - Remove `enableI18n` from NormalizedRestServerConfig - Add self-registration of i18n REST routes in I18nServicePlugin following the AuthPlugin kernel:ready hook pattern - Update rest.test.ts to verify RestServer no longer registers i18n routes - Add comprehensive tests for I18nServicePlugin route self-registration Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
… I18nServicePlugin Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
The createDispatcherPlugin() bridges all service domains (auth, analytics, packages, storage, automation) from HttpDispatcher to the HTTP server, but i18n was missing. When no explicit I18nServicePlugin is loaded (kernel uses memory fallback i18n), no i18n HTTP routes were registered, causing 404. Add 3 i18n route bridges consistent with all other service domains: - GET /api/v1/i18n/locales - GET /api/v1/i18n/translations/:locale - GET /api/v1/i18n/labels/:object/:locale Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Moves i18n HTTP route ownership out of the protocol-level RestServer and into the i18n service layer, while also adding dispatcher bridge routes so i18n endpoints continue working when only the kernel’s fallback i18n service is present.
Changes:
- Removed
enableI18nfromRestApiConfigSchemaand deleted RestServer’s i18n endpoint registration. - Added self-registering i18n routes to
I18nServicePlugin(withregisterRoutes+basePathoptions) and a new test suite. - Added i18n bridge routes to
createDispatcherPlugin()and updated RestServer tests + docs (ROADMAP/CHANGELOG).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/spec/src/api/rest-server.zod.ts | Removes enableI18n flag from REST API config schema. |
| packages/services/service-i18n/src/i18n-service-plugin.ts | Adds kernel:ready-based self-registration for i18n HTTP routes. |
| packages/services/service-i18n/src/i18n-service-plugin.test.ts | Adds tests for i18n route self-registration and handler behavior. |
| packages/runtime/src/dispatcher-plugin.ts | Adds i18n HTTP route bridges delegating to HttpDispatcher.handleI18n(). |
| packages/rest/src/rest.test.ts | Updates expectations: RestServer no longer registers i18n routes. |
| packages/rest/src/rest-server.ts | Removes i18n config and endpoint registration from RestServer. |
| ROADMAP.md | Notes i18n service now self-registers REST routes. |
| CHANGELOG.md | Documents architectural move + dispatcher bridge addition. |
| // GET /i18n/labels/:object/:locale | ||
| httpServer.get(`${basePath}/labels/:object/:locale`, async (req: IHttpRequest, res: IHttpResponse) => { | ||
| try { | ||
| const objectName = req.params.object; | ||
| const locale = req.params.locale; | ||
| if (!objectName || !locale) { | ||
| res.status(400).json({ error: 'Missing object or locale parameter' }); | ||
| return; | ||
| } |
There was a problem hiding this comment.
I18nServicePlugin only registers GET ${basePath}/labels/:object/:locale, but HttpDispatcher.handleI18n (and ObjectStackClient.i18n.getFieldLabels) also supports/uses GET /labels/:object?locale=.... As-is, client requests will 404. Consider registering an additional GET ${basePath}/labels/:object route that reads req.query.locale, or otherwise support both route shapes.
| server.get(`${prefix}/i18n/translations/:locale`, async (req: any, res: any) => { | ||
| try { | ||
| const result = await dispatcher.handleI18n(`/translations/${req.params.locale}`, 'GET', req.query, { request: req }); | ||
| sendResult(result, res); | ||
| } catch (err: any) { | ||
| errorResponse(err, res); | ||
| } | ||
| }); |
There was a problem hiding this comment.
The dispatcher i18n bridges only expose /i18n/translations/:locale and /i18n/labels/:object/:locale. HttpDispatcher.handleI18n explicitly supports (and the TS client uses) /i18n/translations?locale=... and /i18n/labels/:object?locale=..., so those client calls will still 404 even after this change. Add bridge routes for the query-param forms (and keep behavior consistent across both).
|
|
||
| beforeEach(() => { | ||
| httpServer = createMockHttpServer(); | ||
| ctx = createMockPluginContext({ 'http-server': httpServer }); |
There was a problem hiding this comment.
Test setup registers the HTTP server under the 'http-server' name, but production code commonly registers it as 'http.server' (with 'http-server' only as an alias in some server plugins). To catch real regressions, include 'http.server' in the mocked services (and update the plugin to prefer it).
| ctx = createMockPluginContext({ 'http-server': httpServer }); | |
| ctx = createMockPluginContext({ 'http-server': httpServer, 'http.server': httpServer }); |
| expect(res._data).toEqual({ | ||
| data: { | ||
| locales: [ | ||
| { code: 'en', label: 'en', isDefault: true }, | ||
| { code: 'zh-CN', label: 'zh-CN', isDefault: false }, | ||
| ], | ||
| }, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
These assertions lock in a non-standard response shape ({ data: ... }). The repo's JS client unwraps responses based on a { success, data } envelope, so once the plugin is aligned with the standard envelope, these tests should assert the enveloped shape (and error shape) to prevent future incompatibilities.
| httpServer = ctx.getService<IHttpServer>('http-server'); | ||
| } catch { | ||
| // Service not found — expected in MSW/mock mode | ||
| } | ||
|
|
There was a problem hiding this comment.
I18nServicePlugin looks up the HTTP server using service name 'http-server', but the canonical registration is 'http.server' (and external RuntimeConfig.server only registers 'http.server'). This can prevent route self-registration in real runtimes where the alias isn't present. Prefer resolving 'http.server' first (and optionally falling back to 'http-server' for backwards compatibility).
| httpServer = ctx.getService<IHttpServer>('http-server'); | |
| } catch { | |
| // Service not found — expected in MSW/mock mode | |
| } | |
| // Prefer canonical service name used by RuntimeConfig.server | |
| httpServer = ctx.getService<IHttpServer>('http.server'); | |
| } catch { | |
| // Service not found under canonical name — expected in MSW/mock mode or legacy setups | |
| } | |
| if (!httpServer) { | |
| try { | |
| // Backwards compatibility: fall back to legacy alias | |
| httpServer = ctx.getService<IHttpServer>('http-server'); | |
| } catch { | |
| // Service not found under legacy alias either | |
| } | |
| } |
| res.json({ | ||
| data: { | ||
| locales: locales.map((code) => ({ | ||
| code, | ||
| label: code, | ||
| isDefault: code === defaultLocale, | ||
| })), | ||
| }, | ||
| }); | ||
| } catch (error: any) { | ||
| res.status(500).json({ error: error.message }); | ||
| } |
There was a problem hiding this comment.
These route handlers return { data: ... } / { error: ... }, which does not match the standard REST envelope used by ObjectStackClient.unwrapResponse ({ success: boolean, data, meta? } / { success:false, error:{message,code} }). This will break client.i18n calls when served by I18nServicePlugin directly. Align the response shape with the standard envelope (and keep it consistent with HttpDispatcher.handleI18n / dispatcher-plugin).
| // GET /i18n/translations/:locale | ||
| httpServer.get(`${basePath}/translations/:locale`, async (req: IHttpRequest, res: IHttpResponse) => { | ||
| try { | ||
| const locale = req.params.locale; | ||
| if (!locale) { | ||
| res.status(400).json({ error: 'Missing locale parameter' }); | ||
| return; | ||
| } | ||
| const translations = i18n.getTranslations(locale); | ||
| res.json({ data: { locale, translations } }); |
There was a problem hiding this comment.
I18nServicePlugin only registers GET ${basePath}/translations/:locale, but HttpDispatcher.handleI18n (and ObjectStackClient.i18n.getTranslations) also supports/uses the query form GET /translations?locale=.... With the current routing, client requests to /translations?locale=... will 404. Consider registering an additional GET ${basePath}/translations route (reading req.query.locale) or supporting both patterns.
RestServer was registering
/api/v1/i18n/*routes directly, breaking the architectural invariant that service plugins self-register their own HTTP routes. Additionally,createDispatcherPlugin()bridged every service domain (auth, analytics, packages, storage, automation) except i18n — so when the kernel uses the memory fallback (no explicitI18nServicePlugin), i18n endpoints returned 404.RestServer — removed i18n responsibility
enableI18nflag fromRestApiConfigSchemaregisterI18nEndpoints()and all i18n config fromNormalizedRestServerConfigI18nServicePlugin — self-registers routes
registerRoutes(defaulttrue) andbasePath(default/api/v1/i18n) optionskernel:readyhook, same pattern asAuthPluginDispatcher plugin — i18n bridge (fixes the 404)
createDispatcherPlugin(), delegating toHttpDispatcher.handleI18n()Tests
I18nServicePluginroute self-registration and handler behaviorrest.test.tsto assert RestServer no longer registers i18n routesOriginal prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.