@gravito/cosmos 1.0.0-alpha.6 → 1.0.0
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/CHANGELOG.md +8 -0
- package/README.md +3 -3
- package/README.zh-TW.md +1 -1
- package/dist/index.js +4 -2
- package/ion/src/index.js +2 -2
- package/package.json +8 -5
- package/src/I18nService.ts +8 -4
- package/src/index.ts +13 -5
- package/tests/loader.test.ts +44 -0
- package/tests/service.test.ts +109 -0
- package/tsconfig.json +11 -4
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Lightweight Internationalization (i18n) Orbit for Gravito.
|
|
4
4
|
|
|
5
|
-
Provides simple JSON-based localization support for your Gravito application, with seamless integration into
|
|
5
|
+
Provides simple JSON-based localization support for your Gravito application, with seamless integration into Photon context.
|
|
6
6
|
|
|
7
7
|
## 📦 Installation
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ bun add @gravito/cosmos
|
|
|
14
14
|
|
|
15
15
|
1. **Register the Orbit**:
|
|
16
16
|
```typescript
|
|
17
|
-
import { PlanetCore } from 'gravito
|
|
17
|
+
import { PlanetCore } from '@gravito/core';
|
|
18
18
|
import { OrbitI18n } from '@gravito/cosmos';
|
|
19
19
|
|
|
20
20
|
const core = new PlanetCore();
|
|
@@ -50,7 +50,7 @@ bun add @gravito/cosmos
|
|
|
50
50
|
|
|
51
51
|
## ✨ Features
|
|
52
52
|
|
|
53
|
-
- **Context Injection**: Automatically injects `t()` helper into
|
|
53
|
+
- **Context Injection**: Automatically injects `t()` helper into Photon context.
|
|
54
54
|
- **Parameter Replacement**: Supports `{key}` style parameter replacement.
|
|
55
55
|
- **Locale Detection**: Automatically detects locale from `Accept-Language` header or query parameter `?lang=`.
|
|
56
56
|
- **Fallback**: gracefully falls back to default locale if key is missing.
|
package/README.zh-TW.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -146,7 +146,7 @@ async function loadTranslations(directory) {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
// src/index.ts
|
|
149
|
-
class
|
|
149
|
+
class OrbitCosmos {
|
|
150
150
|
config;
|
|
151
151
|
constructor(config) {
|
|
152
152
|
this.config = config;
|
|
@@ -154,12 +154,14 @@ class I18nOrbit {
|
|
|
154
154
|
install(core) {
|
|
155
155
|
const i18nManager = new I18nManager(this.config);
|
|
156
156
|
core.adapter.use("*", localeMiddleware(i18nManager));
|
|
157
|
-
core.logger.info(`I18n
|
|
157
|
+
core.logger.info(`[OrbitCosmos] I18n initialized with locale: ${this.config.defaultLocale}`);
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
+
var I18nOrbit = OrbitCosmos;
|
|
160
161
|
export {
|
|
161
162
|
localeMiddleware,
|
|
162
163
|
loadTranslations,
|
|
164
|
+
OrbitCosmos,
|
|
163
165
|
I18nOrbit,
|
|
164
166
|
I18nManager,
|
|
165
167
|
I18nInstance
|
package/ion/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
3
|
// ../core/package.json
|
|
4
4
|
var package_default = {
|
|
5
|
-
name: "gravito
|
|
5
|
+
name: "@gravito/core",
|
|
6
6
|
version: "1.0.0-beta.2",
|
|
7
7
|
description: "",
|
|
8
8
|
module: "./dist/index.mjs",
|
|
@@ -700,7 +700,7 @@ async function handleProcessError(kind, error) {
|
|
|
700
700
|
}
|
|
701
701
|
}));
|
|
702
702
|
} catch (e) {
|
|
703
|
-
console.error("[gravito
|
|
703
|
+
console.error("[@gravito/core] Failed to handle process-level error:", e);
|
|
704
704
|
} finally {
|
|
705
705
|
if (shouldExit) {
|
|
706
706
|
clearTimeout(exitTimer);
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravito/cosmos",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Internationalization orbit for Gravito framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"build": "bun build ./src/index.ts --outdir ./dist --target node --external @gravito/core --external
|
|
9
|
-
"test": "bun test"
|
|
8
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node --external @gravito/core --external @gravito/photon",
|
|
9
|
+
"test": "bun test",
|
|
10
|
+
"test:coverage": "bun test --coverage --coverage-threshold=80",
|
|
11
|
+
"test:ci": "bun test --coverage --coverage-threshold=80",
|
|
12
|
+
"typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck"
|
|
10
13
|
},
|
|
11
14
|
"keywords": [
|
|
12
15
|
"gravito",
|
|
@@ -17,8 +20,8 @@
|
|
|
17
20
|
"author": "Carl Lee <carllee0520@gmail.com>",
|
|
18
21
|
"license": "MIT",
|
|
19
22
|
"peerDependencies": {
|
|
20
|
-
"gravito
|
|
21
|
-
"
|
|
23
|
+
"@gravito/core": "workspace:*",
|
|
24
|
+
"@gravito/photon": "workspace:*"
|
|
22
25
|
},
|
|
23
26
|
"devDependencies": {
|
|
24
27
|
"bun-types": "latest"
|
package/src/I18nService.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import type { MiddlewareHandler } from '
|
|
1
|
+
import type { MiddlewareHandler } from '@gravito/photon'
|
|
2
|
+
|
|
3
|
+
export type TranslationMap = {
|
|
4
|
+
[key: string]: string | TranslationMap
|
|
5
|
+
}
|
|
2
6
|
|
|
3
7
|
export interface I18nConfig {
|
|
4
8
|
defaultLocale: string
|
|
5
9
|
supportedLocales: string[]
|
|
6
10
|
// Path to translation files, or a Record of translations
|
|
7
11
|
// If undefined, it will look into `resources/lang` by default (conceptually, handled by loader)
|
|
8
|
-
translations?: Record<string,
|
|
12
|
+
translations?: Record<string, TranslationMap>
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
export interface I18nService {
|
|
@@ -103,7 +107,7 @@ export class I18nInstance implements I18nService {
|
|
|
103
107
|
* Holds shared configuration and translation resources
|
|
104
108
|
*/
|
|
105
109
|
export class I18nManager implements I18nService {
|
|
106
|
-
private translations: Record<string,
|
|
110
|
+
private translations: Record<string, TranslationMap> = {}
|
|
107
111
|
// Default instance for global usage (e.g. CLI or background jobs)
|
|
108
112
|
private globalInstance: I18nInstance
|
|
109
113
|
|
|
@@ -195,7 +199,7 @@ export class I18nManager implements I18nService {
|
|
|
195
199
|
* @param locale - The locale string.
|
|
196
200
|
* @param translations - The translations object.
|
|
197
201
|
*/
|
|
198
|
-
addResource(locale: string, translations:
|
|
202
|
+
addResource(locale: string, translations: TranslationMap) {
|
|
199
203
|
this.translations[locale] = {
|
|
200
204
|
...(this.translations[locale] || {}),
|
|
201
205
|
...translations,
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import type { GravitoOrbit, PlanetCore } from 'gravito
|
|
1
|
+
import type { GravitoMiddleware, GravitoOrbit, PlanetCore } from '@gravito/core'
|
|
2
2
|
import { type I18nConfig, I18nManager, type I18nService, localeMiddleware } from './I18nService'
|
|
3
3
|
|
|
4
|
-
declare module 'gravito
|
|
4
|
+
declare module '@gravito/core' {
|
|
5
5
|
interface Variables {
|
|
6
6
|
i18n: I18nService
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* OrbitCosmos - Internationalization Orbit
|
|
12
|
+
*
|
|
13
|
+
* Provides i18n functionality for Gravito applications.
|
|
14
|
+
*/
|
|
15
|
+
export class OrbitCosmos implements GravitoOrbit {
|
|
11
16
|
constructor(private config: I18nConfig) {}
|
|
12
17
|
|
|
13
18
|
install(core: PlanetCore): void {
|
|
@@ -18,11 +23,14 @@ export class I18nOrbit implements GravitoOrbit {
|
|
|
18
23
|
|
|
19
24
|
// Inject locale middleware into every request
|
|
20
25
|
// This middleware handles cloning the i18n instance per request
|
|
21
|
-
core.adapter.use('*', localeMiddleware(i18nManager) as
|
|
26
|
+
core.adapter.use('*', localeMiddleware(i18nManager) as unknown as GravitoMiddleware)
|
|
22
27
|
|
|
23
|
-
core.logger.info(`I18n
|
|
28
|
+
core.logger.info(`[OrbitCosmos] I18n initialized with locale: ${this.config.defaultLocale}`)
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
/** @deprecated Use OrbitCosmos instead */
|
|
33
|
+
export const I18nOrbit = OrbitCosmos
|
|
34
|
+
|
|
27
35
|
export * from './I18nService'
|
|
28
36
|
export * from './loader'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it, jest } from 'bun:test'
|
|
2
|
+
import { mkdtempSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { rm } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { loadTranslations } from '../src/loader'
|
|
7
|
+
|
|
8
|
+
describe('loadTranslations', () => {
|
|
9
|
+
it('loads json translation files from a directory', async () => {
|
|
10
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'gravito-i18n-'))
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
writeFileSync(join(tmpDir, 'en.json'), JSON.stringify({ hello: 'Hello' }))
|
|
14
|
+
writeFileSync(join(tmpDir, 'zh.json'), JSON.stringify({ hello: '你好' }))
|
|
15
|
+
|
|
16
|
+
const translations = await loadTranslations(tmpDir)
|
|
17
|
+
|
|
18
|
+
expect(translations.en.hello).toBe('Hello')
|
|
19
|
+
expect(translations.zh.hello).toBe('你好')
|
|
20
|
+
} finally {
|
|
21
|
+
await rm(tmpDir, { force: true, recursive: true })
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('skips invalid json files and returns empty for missing dir', async () => {
|
|
26
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'gravito-i18n-invalid-'))
|
|
27
|
+
|
|
28
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
29
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
writeFileSync(join(tmpDir, 'en.json'), '{invalid json}')
|
|
33
|
+
const translations = await loadTranslations(tmpDir)
|
|
34
|
+
expect(translations.en).toBeUndefined()
|
|
35
|
+
|
|
36
|
+
const missing = await loadTranslations(join(tmpDir, 'missing'))
|
|
37
|
+
expect(missing).toEqual({})
|
|
38
|
+
} finally {
|
|
39
|
+
errorSpy.mockRestore()
|
|
40
|
+
warnSpy.mockRestore()
|
|
41
|
+
await rm(tmpDir, { force: true, recursive: true })
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it, jest } from 'bun:test'
|
|
2
|
+
import { I18nInstance, I18nManager, localeMiddleware } from '../src/I18nService'
|
|
3
|
+
import { I18nOrbit, OrbitCosmos } from '../src/index'
|
|
4
|
+
|
|
5
|
+
describe('I18nManager and I18nInstance', () => {
|
|
6
|
+
const baseConfig = {
|
|
7
|
+
defaultLocale: 'en',
|
|
8
|
+
supportedLocales: ['en', 'fr'],
|
|
9
|
+
translations: {
|
|
10
|
+
en: {
|
|
11
|
+
greet: 'Hi :name',
|
|
12
|
+
nested: {
|
|
13
|
+
title: 'Title',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
it('adds resources and translates with replacements', () => {
|
|
20
|
+
const manager = new I18nManager(baseConfig as any)
|
|
21
|
+
manager.addResource('fr', { greet: 'Salut :name' })
|
|
22
|
+
manager.locale = 'fr'
|
|
23
|
+
expect(manager.t('greet', { name: 'Carl' })).toBe('Salut Carl')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('clones instances with explicit locale', () => {
|
|
27
|
+
const manager = new I18nManager(baseConfig as any)
|
|
28
|
+
const instance = manager.clone('fr') as I18nInstance
|
|
29
|
+
expect(instance.getLocale()).toBe('fr')
|
|
30
|
+
expect(instance.has('missing.key')).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns key when value is not a string', () => {
|
|
34
|
+
const manager = new I18nManager(baseConfig as any)
|
|
35
|
+
expect(manager.t('nested')).toBe('nested')
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('localeMiddleware', () => {
|
|
40
|
+
it('prefers route param locale and injects i18n', async () => {
|
|
41
|
+
const manager = new I18nManager({
|
|
42
|
+
defaultLocale: 'en',
|
|
43
|
+
supportedLocales: ['en', 'zh'],
|
|
44
|
+
translations: {
|
|
45
|
+
en: { title: 'Hello' },
|
|
46
|
+
zh: { title: '你好' },
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
const middleware = localeMiddleware(manager)
|
|
50
|
+
const ctx = {
|
|
51
|
+
req: {
|
|
52
|
+
param: jest.fn(() => 'zh'),
|
|
53
|
+
query: jest.fn(() => undefined),
|
|
54
|
+
header: jest.fn(() => undefined),
|
|
55
|
+
},
|
|
56
|
+
set: jest.fn(),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await middleware(ctx as any, async () => {})
|
|
60
|
+
|
|
61
|
+
const injected = ctx.set.mock.calls[0][1]
|
|
62
|
+
expect(ctx.set).toHaveBeenCalledWith('i18n', expect.any(I18nInstance))
|
|
63
|
+
expect(injected.locale).toBe('zh')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('uses default locale when none provided', async () => {
|
|
67
|
+
const manager = new I18nManager({
|
|
68
|
+
defaultLocale: 'en',
|
|
69
|
+
supportedLocales: ['en'],
|
|
70
|
+
translations: {
|
|
71
|
+
en: { title: 'Hello' },
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
const middleware = localeMiddleware(manager)
|
|
75
|
+
const ctx = {
|
|
76
|
+
req: {
|
|
77
|
+
param: jest.fn(() => undefined),
|
|
78
|
+
query: jest.fn(() => undefined),
|
|
79
|
+
header: jest.fn(() => undefined),
|
|
80
|
+
},
|
|
81
|
+
set: jest.fn(),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await middleware(ctx as any, async () => {})
|
|
85
|
+
|
|
86
|
+
const injected = ctx.set.mock.calls[0][1]
|
|
87
|
+
expect(injected.locale).toBe('en')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('OrbitCosmos', () => {
|
|
92
|
+
it('installs locale middleware and logs initialization', () => {
|
|
93
|
+
const core = {
|
|
94
|
+
adapter: { use: jest.fn() },
|
|
95
|
+
logger: { info: jest.fn() },
|
|
96
|
+
}
|
|
97
|
+
const orbit = new OrbitCosmos({
|
|
98
|
+
defaultLocale: 'en',
|
|
99
|
+
supportedLocales: ['en'],
|
|
100
|
+
translations: { en: { title: 'Hello' } },
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
orbit.install(core as any)
|
|
104
|
+
|
|
105
|
+
expect(core.adapter.use).toHaveBeenCalledWith('*', expect.any(Function))
|
|
106
|
+
expect(core.logger.info).toHaveBeenCalledWith('[OrbitCosmos] I18n initialized with locale: en')
|
|
107
|
+
expect(I18nOrbit).toBe(OrbitCosmos)
|
|
108
|
+
})
|
|
109
|
+
})
|
package/tsconfig.json
CHANGED
|
@@ -4,9 +4,16 @@
|
|
|
4
4
|
"outDir": "./dist",
|
|
5
5
|
"baseUrl": ".",
|
|
6
6
|
"paths": {
|
|
7
|
-
"gravito
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
"@gravito/core": [
|
|
8
|
+
"../../packages/core/src/index.ts"
|
|
9
|
+
],
|
|
10
|
+
"@gravito/*": [
|
|
11
|
+
"../../packages/*/src/index.ts"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"types": [
|
|
15
|
+
"bun-types"
|
|
16
|
+
]
|
|
10
17
|
},
|
|
11
18
|
"include": [
|
|
12
19
|
"src/**/*"
|
|
@@ -16,4 +23,4 @@
|
|
|
16
23
|
"dist",
|
|
17
24
|
"**/*.test.ts"
|
|
18
25
|
]
|
|
19
|
-
}
|
|
26
|
+
}
|