@worldware/msg 0.6.4 → 0.7.1
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 +59 -18
- package/dist/{chunk-AWSZFTAJ.mjs → chunk-2QUOYESW.mjs} +33 -0
- package/dist/classes/MsgProject/MsgProject.d.cts +1 -0
- package/dist/classes/MsgProject/MsgProject.d.ts +1 -0
- package/dist/classes/MsgResource/MsgResource.cjs +33 -0
- package/dist/classes/MsgResource/MsgResource.mjs +1 -1
- package/dist/classes/index.cjs +33 -0
- package/dist/classes/index.mjs +1 -1
- package/dist/index.cjs +33 -0
- package/dist/index.mjs +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ A TypeScript library for managing internationalization (i18n) messages with supp
|
|
|
8
8
|
|
|
9
9
|
- **Message Management**: Organize messages into resources with keys and values
|
|
10
10
|
- **Translation Loading**: Load translations from external sources via customizable loaders
|
|
11
|
+
- **Pseudo Localization**: Request a pseudolocalized resource for UI testing via `getTranslation(pseudoLocale)`
|
|
11
12
|
- **Message Formatting**: Format messages with parameters using MessageFormat 2 (MF2) syntax
|
|
12
13
|
- **Attributes & Notes**: Attach metadata (language, direction, do-not-translate flags) and notes to messages
|
|
13
14
|
- **Project Configuration**: Configure projects with locale settings and translation loaders
|
|
@@ -25,6 +26,7 @@ npm install @worldware/msg
|
|
|
25
26
|
A project configuration that defines:
|
|
26
27
|
- Project name and version
|
|
27
28
|
- Source and target locales (with language fallback chains)
|
|
29
|
+
- Pseudo locale (for pseudolocalized output via `getTranslation`)
|
|
28
30
|
- A translation loader function
|
|
29
31
|
|
|
30
32
|
### MsgResource
|
|
@@ -48,15 +50,30 @@ An individual message with:
|
|
|
48
50
|
|
|
49
51
|
### Basic Setup
|
|
50
52
|
|
|
53
|
+
The following example matches the ES module output of the **msg-cli** `create project` command—a typical project file that loads translations from JSON under a translations directory:
|
|
54
|
+
|
|
51
55
|
```typescript
|
|
52
|
-
import { MsgProject
|
|
56
|
+
import { MsgProject } from '@worldware/msg';
|
|
57
|
+
|
|
58
|
+
const TRANSLATION_IMPORT_PATH = '../l10n/translations';
|
|
59
|
+
const loader = async (project, title, language) => {
|
|
60
|
+
const path = `${TRANSLATION_IMPORT_PATH}/${project}/${language}/${title}.json`;
|
|
61
|
+
try {
|
|
62
|
+
const module = await import(path, { with: { type: 'json' } });
|
|
63
|
+
return module.default;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn(`Translations for locale ${language} could not be loaded.`, error);
|
|
66
|
+
return {
|
|
67
|
+
title,
|
|
68
|
+
attributes: { lang: language, dir: '' },
|
|
69
|
+
notes: [],
|
|
70
|
+
messages: []
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
};
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
project: {
|
|
57
|
-
name: 'my-app',
|
|
58
|
-
version: 1
|
|
59
|
-
},
|
|
75
|
+
export default MsgProject.create({
|
|
76
|
+
project: { name: 'my-app', version: 1 },
|
|
60
77
|
locales: {
|
|
61
78
|
sourceLocale: 'en',
|
|
62
79
|
pseudoLocale: 'en-XA',
|
|
@@ -64,18 +81,15 @@ const project = MsgProject.create({
|
|
|
64
81
|
'en': ['en'],
|
|
65
82
|
'es': ['es'],
|
|
66
83
|
'fr': ['fr'],
|
|
67
|
-
'fr-CA': ['fr', 'fr-CA']
|
|
84
|
+
'fr-CA': ['fr', 'fr-CA']
|
|
68
85
|
}
|
|
69
86
|
},
|
|
70
|
-
loader
|
|
71
|
-
// Custom loader to fetch translation data
|
|
72
|
-
const path = `./translations/${project}/${lang}/${title}.json`;
|
|
73
|
-
const data = await import(path);
|
|
74
|
-
return data;
|
|
75
|
-
}
|
|
87
|
+
loader
|
|
76
88
|
});
|
|
77
89
|
```
|
|
78
90
|
|
|
91
|
+
When using this in your app, import the default export as your project and pass it to `MsgResource.create` (see below).
|
|
92
|
+
|
|
79
93
|
### Creating a Resource
|
|
80
94
|
|
|
81
95
|
```typescript
|
|
@@ -89,7 +103,7 @@ const resource = MsgResource.create({
|
|
|
89
103
|
messages: [
|
|
90
104
|
{
|
|
91
105
|
key: 'greeting',
|
|
92
|
-
value: 'Hello, {name}!'
|
|
106
|
+
value: 'Hello, {$name}!'
|
|
93
107
|
},
|
|
94
108
|
{
|
|
95
109
|
key: 'welcome',
|
|
@@ -99,7 +113,7 @@ const resource = MsgResource.create({
|
|
|
99
113
|
}, project);
|
|
100
114
|
|
|
101
115
|
// Or add messages programmatically
|
|
102
|
-
resource.add('goodbye', 'Goodbye, {name}!', {
|
|
116
|
+
resource.add('goodbye', 'Goodbye, {$name}!', {
|
|
103
117
|
lang: 'en',
|
|
104
118
|
dir: 'ltr'
|
|
105
119
|
});
|
|
@@ -124,11 +138,38 @@ const spanishResource = await resource.getTranslation('es');
|
|
|
124
138
|
// falling back to the source messages for missing translations
|
|
125
139
|
```
|
|
126
140
|
|
|
141
|
+
### Language fallbacks and translation layering
|
|
142
|
+
|
|
143
|
+
The project's `targetLocales` maps each requested locale to a **fallback chain**: an array of locale codes ordered from least specific to most specific (e.g. base language first, then region-specific). For example, `'zh-HK': ['zh', 'zh-Hant', 'zh-HK']` means that when you request `zh-HK`, the chain is first `zh`, then `zh-Hant`, then `zh-HK`. You can get the chain for any locale with `project.getTargetLocale(locale)`.
|
|
144
|
+
|
|
145
|
+
When you call `resource.getTranslation(locale)`:
|
|
146
|
+
|
|
147
|
+
1. The **source resource** (the resource you called it on) is the base.
|
|
148
|
+
2. For each locale in that locale's chain, the project **loader** is called to load that locale's translation data.
|
|
149
|
+
3. Each loaded dataset is **layered** onto the current result: messages in the new data add or override by key; keys missing in the new layer keep the value from the previous layer.
|
|
150
|
+
4. The final resource is the result after all layers have been applied.
|
|
151
|
+
|
|
152
|
+
So for `getTranslation('zh-HK')` with chain `['zh', 'zh-Hant', 'zh-HK']`, you get: source → then zh overlay → then zh-Hant overlay → then zh-HK overlay. Later entries in the chain override earlier ones for the same key; missing keys fall back to the previous layer (and ultimately to the source).
|
|
153
|
+
|
|
154
|
+
### Pseudo Localization
|
|
155
|
+
|
|
156
|
+
When `getTranslation` is called with the project's `pseudoLocale` (e.g. `en-XA`), it returns a new resource with pseudolocalized message values—useful for testing UI layout and finding hardcoded strings without loading translation files:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Request pseudolocalized messages (project locales.pseudoLocale is 'en-XA')
|
|
160
|
+
const pseudoResource = await resource.getTranslation('en-XA');
|
|
161
|
+
|
|
162
|
+
// Message values are transformed: "Hello, {$name}!" → "Ħḗḗŀŀǿǿ, {$name}!"
|
|
163
|
+
// Variables and MF2 syntax are preserved; only literal text is pseudolocalized
|
|
164
|
+
const greeting = pseudoResource.get('greeting')?.format({ name: 'Alice' });
|
|
165
|
+
// Result: "Ħḗḗŀŀǿǿ, Alice!"
|
|
166
|
+
```
|
|
167
|
+
|
|
127
168
|
### Working with Attributes and Notes
|
|
128
169
|
|
|
129
170
|
```typescript
|
|
130
171
|
// Add notes to messages
|
|
131
|
-
resource.add('complex-message', '
|
|
172
|
+
resource.add('complex-message', 'You have {$count} items', {
|
|
132
173
|
lang: 'en',
|
|
133
174
|
dir: 'ltr',
|
|
134
175
|
dnt: false // do-not-translate flag
|
|
@@ -188,7 +229,7 @@ const data = resource.getData();
|
|
|
188
229
|
**Methods:**
|
|
189
230
|
- `add(key: string, value: string, attributes?: MsgAttributes, notes?: MsgNote[]): MsgResource` - Add a message
|
|
190
231
|
- `translate(data: MsgResourceData): MsgResource` - Create a translated version
|
|
191
|
-
- `getTranslation(lang: string): Promise<MsgResource>` - Load and apply translations
|
|
232
|
+
- `getTranslation(lang: string): Promise<MsgResource>` - Load and apply translations. When `lang` matches the project's `pseudoLocale`, returns a resource with pseudolocalized message values instead of loading from the loader.
|
|
192
233
|
- `getProject(): MsgProject` - Returns the project instance associated with the resource
|
|
193
234
|
- `getData(stripNotes?: boolean): MsgResourceData` - Get resource data. Message objects in the output omit `attributes` when they match the resource's attributes (to avoid redundancy)
|
|
194
235
|
- `toJSON(stripNotes?: boolean): string` - Serialize to JSON
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
} from "./chunk-QWBDIKQK.mjs";
|
|
7
7
|
|
|
8
8
|
// src/classes/MsgResource/MsgResource.ts
|
|
9
|
+
import { parseMessage, stringifyMessage, visit } from "messageformat";
|
|
10
|
+
import { localize } from "pseudo-localization";
|
|
9
11
|
var MsgResource = class _MsgResource extends Map {
|
|
10
12
|
_attributes = {};
|
|
11
13
|
_notes = [];
|
|
@@ -38,6 +40,20 @@ var MsgResource = class _MsgResource extends Map {
|
|
|
38
40
|
const msg = message.attributes;
|
|
39
41
|
return res.lang === msg.lang && res.dir === msg.dir && res.dnt === msg.dnt;
|
|
40
42
|
}
|
|
43
|
+
pseudoLocalizeMF2(source, options) {
|
|
44
|
+
const msg = parseMessage(source);
|
|
45
|
+
visit(msg, {
|
|
46
|
+
pattern: (pattern) => {
|
|
47
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
48
|
+
const part = pattern[i];
|
|
49
|
+
if (typeof part === "string") {
|
|
50
|
+
pattern[i] = localize(part, options);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return stringifyMessage(msg);
|
|
56
|
+
}
|
|
41
57
|
get attributes() {
|
|
42
58
|
return this._attributes;
|
|
43
59
|
}
|
|
@@ -104,6 +120,23 @@ var MsgResource = class _MsgResource extends Map {
|
|
|
104
120
|
}
|
|
105
121
|
async getTranslation(lang) {
|
|
106
122
|
const project = this._project;
|
|
123
|
+
const pseudoLocale = project.locales.pseudoLocale;
|
|
124
|
+
if (lang === pseudoLocale) {
|
|
125
|
+
const pseudolocalizedData = {
|
|
126
|
+
title: this.title,
|
|
127
|
+
attributes: { ...this.attributes, lang: pseudoLocale },
|
|
128
|
+
notes: this.notes.length > 0 ? this.notes : void 0,
|
|
129
|
+
messages: []
|
|
130
|
+
};
|
|
131
|
+
this.forEach((msg) => {
|
|
132
|
+
pseudolocalizedData.messages.push({
|
|
133
|
+
key: msg.key,
|
|
134
|
+
value: this.pseudoLocalizeMF2(msg.value),
|
|
135
|
+
attributes: { ...this.attributes, lang: pseudoLocale }
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
return this.translate(pseudolocalizedData);
|
|
139
|
+
}
|
|
107
140
|
const languageChain = project.getTargetLocale(lang);
|
|
108
141
|
if (!languageChain) {
|
|
109
142
|
throw new Error("Unsupported locale for resource.");
|
|
@@ -46,6 +46,7 @@ declare class MsgResource extends Map<string, MsgMessage> implements MsgInterfac
|
|
|
46
46
|
static create(data: MsgResourceData, project: MsgProject): MsgResource;
|
|
47
47
|
private constructor();
|
|
48
48
|
private hasMatchingAttributes;
|
|
49
|
+
private pseudoLocalizeMF2;
|
|
49
50
|
get attributes(): MsgAttributes;
|
|
50
51
|
set attributes(attributes: MsgAttributes);
|
|
51
52
|
get notes(): MsgNote[];
|
|
@@ -46,6 +46,7 @@ declare class MsgResource extends Map<string, MsgMessage> implements MsgInterfac
|
|
|
46
46
|
static create(data: MsgResourceData, project: MsgProject): MsgResource;
|
|
47
47
|
private constructor();
|
|
48
48
|
private hasMatchingAttributes;
|
|
49
|
+
private pseudoLocalizeMF2;
|
|
49
50
|
get attributes(): MsgAttributes;
|
|
50
51
|
set attributes(attributes: MsgAttributes);
|
|
51
52
|
get notes(): MsgNote[];
|
|
@@ -23,6 +23,8 @@ __export(MsgResource_exports, {
|
|
|
23
23
|
MsgResource: () => MsgResource
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(MsgResource_exports);
|
|
26
|
+
var import_messageformat2 = require("messageformat");
|
|
27
|
+
var import_pseudo_localization = require("pseudo-localization");
|
|
26
28
|
|
|
27
29
|
// src/classes/MsgMessage/MsgMessage.ts
|
|
28
30
|
var import_messageformat = require("messageformat");
|
|
@@ -130,6 +132,20 @@ var MsgResource = class _MsgResource extends Map {
|
|
|
130
132
|
const msg = message.attributes;
|
|
131
133
|
return res.lang === msg.lang && res.dir === msg.dir && res.dnt === msg.dnt;
|
|
132
134
|
}
|
|
135
|
+
pseudoLocalizeMF2(source, options) {
|
|
136
|
+
const msg = (0, import_messageformat2.parseMessage)(source);
|
|
137
|
+
(0, import_messageformat2.visit)(msg, {
|
|
138
|
+
pattern: (pattern) => {
|
|
139
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
140
|
+
const part = pattern[i];
|
|
141
|
+
if (typeof part === "string") {
|
|
142
|
+
pattern[i] = (0, import_pseudo_localization.localize)(part, options);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return (0, import_messageformat2.stringifyMessage)(msg);
|
|
148
|
+
}
|
|
133
149
|
get attributes() {
|
|
134
150
|
return this._attributes;
|
|
135
151
|
}
|
|
@@ -196,6 +212,23 @@ var MsgResource = class _MsgResource extends Map {
|
|
|
196
212
|
}
|
|
197
213
|
async getTranslation(lang) {
|
|
198
214
|
const project = this._project;
|
|
215
|
+
const pseudoLocale = project.locales.pseudoLocale;
|
|
216
|
+
if (lang === pseudoLocale) {
|
|
217
|
+
const pseudolocalizedData = {
|
|
218
|
+
title: this.title,
|
|
219
|
+
attributes: { ...this.attributes, lang: pseudoLocale },
|
|
220
|
+
notes: this.notes.length > 0 ? this.notes : void 0,
|
|
221
|
+
messages: []
|
|
222
|
+
};
|
|
223
|
+
this.forEach((msg) => {
|
|
224
|
+
pseudolocalizedData.messages.push({
|
|
225
|
+
key: msg.key,
|
|
226
|
+
value: this.pseudoLocalizeMF2(msg.value),
|
|
227
|
+
attributes: { ...this.attributes, lang: pseudoLocale }
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
return this.translate(pseudolocalizedData);
|
|
231
|
+
}
|
|
199
232
|
const languageChain = project.getTargetLocale(lang);
|
|
200
233
|
if (!languageChain) {
|
|
201
234
|
throw new Error("Unsupported locale for resource.");
|
package/dist/classes/index.cjs
CHANGED
|
@@ -100,6 +100,8 @@ var MsgMessage = class _MsgMessage {
|
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
// src/classes/MsgResource/MsgResource.ts
|
|
103
|
+
var import_messageformat2 = require("messageformat");
|
|
104
|
+
var import_pseudo_localization = require("pseudo-localization");
|
|
103
105
|
var MsgResource = class _MsgResource extends Map {
|
|
104
106
|
_attributes = {};
|
|
105
107
|
_notes = [];
|
|
@@ -132,6 +134,20 @@ var MsgResource = class _MsgResource extends Map {
|
|
|
132
134
|
const msg = message.attributes;
|
|
133
135
|
return res.lang === msg.lang && res.dir === msg.dir && res.dnt === msg.dnt;
|
|
134
136
|
}
|
|
137
|
+
pseudoLocalizeMF2(source, options) {
|
|
138
|
+
const msg = (0, import_messageformat2.parseMessage)(source);
|
|
139
|
+
(0, import_messageformat2.visit)(msg, {
|
|
140
|
+
pattern: (pattern) => {
|
|
141
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
142
|
+
const part = pattern[i];
|
|
143
|
+
if (typeof part === "string") {
|
|
144
|
+
pattern[i] = (0, import_pseudo_localization.localize)(part, options);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return (0, import_messageformat2.stringifyMessage)(msg);
|
|
150
|
+
}
|
|
135
151
|
get attributes() {
|
|
136
152
|
return this._attributes;
|
|
137
153
|
}
|
|
@@ -198,6 +214,23 @@ var MsgResource = class _MsgResource extends Map {
|
|
|
198
214
|
}
|
|
199
215
|
async getTranslation(lang) {
|
|
200
216
|
const project = this._project;
|
|
217
|
+
const pseudoLocale = project.locales.pseudoLocale;
|
|
218
|
+
if (lang === pseudoLocale) {
|
|
219
|
+
const pseudolocalizedData = {
|
|
220
|
+
title: this.title,
|
|
221
|
+
attributes: { ...this.attributes, lang: pseudoLocale },
|
|
222
|
+
notes: this.notes.length > 0 ? this.notes : void 0,
|
|
223
|
+
messages: []
|
|
224
|
+
};
|
|
225
|
+
this.forEach((msg) => {
|
|
226
|
+
pseudolocalizedData.messages.push({
|
|
227
|
+
key: msg.key,
|
|
228
|
+
value: this.pseudoLocalizeMF2(msg.value),
|
|
229
|
+
attributes: { ...this.attributes, lang: pseudoLocale }
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
return this.translate(pseudolocalizedData);
|
|
233
|
+
}
|
|
201
234
|
const languageChain = project.getTargetLocale(lang);
|
|
202
235
|
if (!languageChain) {
|
|
203
236
|
throw new Error("Unsupported locale for resource.");
|
package/dist/classes/index.mjs
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -100,6 +100,8 @@ var MsgMessage = class _MsgMessage {
|
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
// src/classes/MsgResource/MsgResource.ts
|
|
103
|
+
var import_messageformat2 = require("messageformat");
|
|
104
|
+
var import_pseudo_localization = require("pseudo-localization");
|
|
103
105
|
var MsgResource = class _MsgResource extends Map {
|
|
104
106
|
_attributes = {};
|
|
105
107
|
_notes = [];
|
|
@@ -132,6 +134,20 @@ var MsgResource = class _MsgResource extends Map {
|
|
|
132
134
|
const msg = message.attributes;
|
|
133
135
|
return res.lang === msg.lang && res.dir === msg.dir && res.dnt === msg.dnt;
|
|
134
136
|
}
|
|
137
|
+
pseudoLocalizeMF2(source, options) {
|
|
138
|
+
const msg = (0, import_messageformat2.parseMessage)(source);
|
|
139
|
+
(0, import_messageformat2.visit)(msg, {
|
|
140
|
+
pattern: (pattern) => {
|
|
141
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
142
|
+
const part = pattern[i];
|
|
143
|
+
if (typeof part === "string") {
|
|
144
|
+
pattern[i] = (0, import_pseudo_localization.localize)(part, options);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return (0, import_messageformat2.stringifyMessage)(msg);
|
|
150
|
+
}
|
|
135
151
|
get attributes() {
|
|
136
152
|
return this._attributes;
|
|
137
153
|
}
|
|
@@ -198,6 +214,23 @@ var MsgResource = class _MsgResource extends Map {
|
|
|
198
214
|
}
|
|
199
215
|
async getTranslation(lang) {
|
|
200
216
|
const project = this._project;
|
|
217
|
+
const pseudoLocale = project.locales.pseudoLocale;
|
|
218
|
+
if (lang === pseudoLocale) {
|
|
219
|
+
const pseudolocalizedData = {
|
|
220
|
+
title: this.title,
|
|
221
|
+
attributes: { ...this.attributes, lang: pseudoLocale },
|
|
222
|
+
notes: this.notes.length > 0 ? this.notes : void 0,
|
|
223
|
+
messages: []
|
|
224
|
+
};
|
|
225
|
+
this.forEach((msg) => {
|
|
226
|
+
pseudolocalizedData.messages.push({
|
|
227
|
+
key: msg.key,
|
|
228
|
+
value: this.pseudoLocalizeMF2(msg.value),
|
|
229
|
+
attributes: { ...this.attributes, lang: pseudoLocale }
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
return this.translate(pseudolocalizedData);
|
|
233
|
+
}
|
|
201
234
|
const languageChain = project.getTargetLocale(lang);
|
|
202
235
|
if (!languageChain) {
|
|
203
236
|
throw new Error("Unsupported locale for resource.");
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@worldware/msg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Message localization tooling",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Joel Sahleen",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"vitest": "^4.0.15"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"messageformat": "4.0.0-10"
|
|
41
|
+
"messageformat": "4.0.0-10",
|
|
42
|
+
"pseudo-localization": "^2.4.0"
|
|
42
43
|
},
|
|
43
44
|
"scripts": {
|
|
44
45
|
"build": "tsup",
|