@objectstack/service-i18n 4.0.3 → 4.0.5

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.
@@ -1,198 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { Plugin, PluginContext } from '@objectstack/core';
4
- import type { IHttpServer, IHttpRequest, IHttpResponse } from '@objectstack/spec/contracts';
5
- import type { II18nService } from '@objectstack/spec/contracts';
6
- import { FileI18nAdapter } from './file-i18n-adapter.js';
7
- import type { FileI18nAdapterOptions } from './file-i18n-adapter.js';
8
-
9
- /**
10
- * Configuration options for the I18nServicePlugin.
11
- */
12
- export interface I18nServicePluginOptions {
13
- /** Default locale (default: 'en') */
14
- defaultLocale?: string;
15
- /** Directory containing locale JSON files */
16
- localesDir?: string;
17
- /** Fallback locale for missing translations */
18
- fallbackLocale?: string;
19
- /**
20
- * Whether to automatically register i18n REST routes with the HTTP server.
21
- * When true (default), the plugin registers `/api/v1/i18n/*` endpoints
22
- * via the `kernel:ready` hook. When false or no HTTP server is available,
23
- * routes are skipped but the i18n service is still available via the kernel.
24
- * @default true
25
- */
26
- registerRoutes?: boolean;
27
- /**
28
- * Base path for i18n REST routes.
29
- * @default '/api/v1/i18n'
30
- */
31
- basePath?: string;
32
- }
33
-
34
- /**
35
- * I18nServicePlugin — Production II18nService implementation.
36
- *
37
- * Registers an i18n service with the kernel during the init phase,
38
- * and self-registers REST endpoints (`/api/v1/i18n/*`) with the HTTP
39
- * server during the `kernel:ready` hook.
40
- *
41
- * REST route self-registration follows the same autonomous plugin pattern
42
- * used by AuthPlugin, WorkflowPlugin, and other service plugins — RestServer
43
- * is not involved.
44
- *
45
- * @example
46
- * ```ts
47
- * import { ObjectKernel } from '@objectstack/core';
48
- * import { I18nServicePlugin } from '@objectstack/service-i18n';
49
- *
50
- * const kernel = new ObjectKernel();
51
- * kernel.use(new I18nServicePlugin({
52
- * defaultLocale: 'en',
53
- * localesDir: './i18n',
54
- * fallbackLocale: 'en',
55
- * }));
56
- * await kernel.bootstrap();
57
- *
58
- * const i18n = kernel.getService('i18n');
59
- * i18n.t('objects.account.label', 'en'); // 'Account'
60
- * ```
61
- */
62
- export class I18nServicePlugin implements Plugin {
63
- name = 'com.objectstack.service.i18n';
64
- version = '1.0.0';
65
- type = 'standard';
66
-
67
- private readonly options: I18nServicePluginOptions;
68
- private i18n: II18nService | null = null;
69
-
70
- constructor(options: I18nServicePluginOptions = {}) {
71
- this.options = options;
72
- }
73
-
74
- async init(ctx: PluginContext): Promise<void> {
75
- const adapterOptions: FileI18nAdapterOptions = {
76
- defaultLocale: this.options.defaultLocale,
77
- localesDir: this.options.localesDir,
78
- fallbackLocale: this.options.fallbackLocale,
79
- };
80
-
81
- this.i18n = new FileI18nAdapter(adapterOptions);
82
- ctx.registerService('i18n', this.i18n);
83
- ctx.logger.info(
84
- `I18nServicePlugin: registered file-based i18n adapter (default: ${this.i18n.getDefaultLocale?.() ?? 'en'})`,
85
- );
86
- }
87
-
88
- async start(ctx: PluginContext): Promise<void> {
89
- // Defer HTTP route registration to kernel:ready hook.
90
- // This ensures all plugins (including HonoServerPlugin) have completed
91
- // their init and start phases before we attempt to look up the
92
- // http-server service — making I18nServicePlugin resilient to plugin
93
- // loading order.
94
- if (this.options.registerRoutes !== false) {
95
- ctx.hook('kernel:ready', async () => {
96
- let httpServer: IHttpServer | null = null;
97
- try {
98
- httpServer = ctx.getService<IHttpServer>('http-server');
99
- } catch {
100
- // Service not found — expected in MSW/mock mode
101
- }
102
-
103
- if (httpServer) {
104
- this.registerI18nRoutes(httpServer, ctx);
105
- } else {
106
- ctx.logger.warn(
107
- 'No HTTP server available — i18n routes not registered. ' +
108
- 'i18n service is still available programmatically via kernel.getService("i18n").'
109
- );
110
- }
111
- });
112
- }
113
- }
114
-
115
- /**
116
- * Register i18n REST routes with the HTTP server.
117
- *
118
- * Routes:
119
- * - GET /api/v1/i18n/locales → list available locales
120
- * - GET /api/v1/i18n/translations/:locale → get translations for a locale
121
- * - GET /api/v1/i18n/labels/:object/:locale → get field labels for an object
122
- */
123
- private registerI18nRoutes(httpServer: IHttpServer, ctx: PluginContext): void {
124
- if (!this.i18n) return;
125
-
126
- const basePath = this.options.basePath || '/api/v1/i18n';
127
- const i18n = this.i18n;
128
-
129
- // GET /i18n/locales
130
- httpServer.get(`${basePath}/locales`, async (_req: IHttpRequest, res: IHttpResponse) => {
131
- try {
132
- const locales = i18n.getLocales();
133
- const defaultLocale = i18n.getDefaultLocale?.() ?? 'en';
134
- res.json({
135
- data: {
136
- locales: locales.map((code) => ({
137
- code,
138
- label: code,
139
- isDefault: code === defaultLocale,
140
- })),
141
- },
142
- });
143
- } catch (error: any) {
144
- res.status(500).json({ error: error.message });
145
- }
146
- });
147
-
148
- // GET /i18n/translations/:locale
149
- httpServer.get(`${basePath}/translations/:locale`, async (req: IHttpRequest, res: IHttpResponse) => {
150
- try {
151
- const locale = req.params.locale;
152
- if (!locale) {
153
- res.status(400).json({ error: 'Missing locale parameter' });
154
- return;
155
- }
156
- const translations = i18n.getTranslations(locale);
157
- res.json({ data: { locale, translations } });
158
- } catch (error: any) {
159
- res.status(500).json({ error: error.message });
160
- }
161
- });
162
-
163
- // GET /i18n/labels/:object/:locale
164
- httpServer.get(`${basePath}/labels/:object/:locale`, async (req: IHttpRequest, res: IHttpResponse) => {
165
- try {
166
- const objectName = req.params.object;
167
- const locale = req.params.locale;
168
- if (!objectName || !locale) {
169
- res.status(400).json({ error: 'Missing object or locale parameter' });
170
- return;
171
- }
172
- // Some implementations may provide a dedicated getFieldLabels method
173
- const hasGetFieldLabels = 'getFieldLabels' in i18n
174
- && typeof (i18n as Record<string, unknown>)['getFieldLabels'] === 'function';
175
- if (hasGetFieldLabels) {
176
- const labels = (i18n as II18nService & { getFieldLabels(obj: string, loc: string): Record<string, string> })
177
- .getFieldLabels(objectName, locale);
178
- res.json({ data: { object: objectName, locale, labels } });
179
- } else {
180
- // Fallback: derive field labels from full translation bundle
181
- const translations = i18n.getTranslations(locale);
182
- const prefix = `o.${objectName}.fields.`;
183
- const labels: Record<string, string> = {};
184
- for (const [key, value] of Object.entries(translations)) {
185
- if (key.startsWith(prefix)) {
186
- labels[key.substring(prefix.length)] = value as string;
187
- }
188
- }
189
- res.json({ data: { object: objectName, locale, labels } });
190
- }
191
- } catch (error: any) {
192
- res.status(500).json({ error: error.message });
193
- }
194
- });
195
-
196
- ctx.logger.info(`I18n routes registered: ${basePath}/locales, ${basePath}/translations/:locale, ${basePath}/labels/:object/:locale`);
197
- }
198
- }
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- export { I18nServicePlugin } from './i18n-service-plugin.js';
4
- export type { I18nServicePluginOptions } from './i18n-service-plugin.js';
5
- export { FileI18nAdapter } from './file-i18n-adapter.js';
6
- export type { FileI18nAdapterOptions } from './file-i18n-adapter.js';
package/tsconfig.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src",
6
- "types": [
7
- "node"
8
- ]
9
- },
10
- "include": [
11
- "src"
12
- ],
13
- "exclude": [
14
- "node_modules",
15
- "dist"
16
- ]
17
- }