@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.
- package/README.md +377 -0
- package/package.json +31 -6
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -162
- package/src/file-i18n-adapter.test.ts +0 -221
- package/src/file-i18n-adapter.ts +0 -193
- package/src/i18n-service-plugin.test.ts +0 -280
- package/src/i18n-service-plugin.ts +0 -198
- package/src/index.ts +0 -6
- package/tsconfig.json +0 -17
|
@@ -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