@worldware/msg 0.6.3 → 0.7.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/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
@@ -89,7 +91,7 @@ const resource = MsgResource.create({
89
91
  messages: [
90
92
  {
91
93
  key: 'greeting',
92
- value: 'Hello, {name}!'
94
+ value: 'Hello, {$name}!'
93
95
  },
94
96
  {
95
97
  key: 'welcome',
@@ -99,7 +101,7 @@ const resource = MsgResource.create({
99
101
  }, project);
100
102
 
101
103
  // Or add messages programmatically
102
- resource.add('goodbye', 'Goodbye, {name}!', {
104
+ resource.add('goodbye', 'Goodbye, {$name}!', {
103
105
  lang: 'en',
104
106
  dir: 'ltr'
105
107
  });
@@ -124,11 +126,25 @@ const spanishResource = await resource.getTranslation('es');
124
126
  // falling back to the source messages for missing translations
125
127
  ```
126
128
 
129
+ ### Pseudo Localization
130
+
131
+ 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:
132
+
133
+ ```typescript
134
+ // Request pseudolocalized messages (project locales.pseudoLocale is 'en-XA')
135
+ const pseudoResource = await resource.getTranslation('en-XA');
136
+
137
+ // Message values are transformed: "Hello, {$name}!" → "Ħḗḗŀŀǿǿ, {$name}!"
138
+ // Variables and MF2 syntax are preserved; only literal text is pseudolocalized
139
+ const greeting = pseudoResource.get('greeting')?.format({ name: 'Alice' });
140
+ // Result: "Ħḗḗŀŀǿǿ, Alice!"
141
+ ```
142
+
127
143
  ### Working with Attributes and Notes
128
144
 
129
145
  ```typescript
130
146
  // Add notes to messages
131
- resource.add('complex-message', 'This is a complex message', {
147
+ resource.add('complex-message', 'You have {$count} items', {
132
148
  lang: 'en',
133
149
  dir: 'ltr',
134
150
  dnt: false // do-not-translate flag
@@ -188,7 +204,7 @@ const data = resource.getData();
188
204
  **Methods:**
189
205
  - `add(key: string, value: string, attributes?: MsgAttributes, notes?: MsgNote[]): MsgResource` - Add a message
190
206
  - `translate(data: MsgResourceData): MsgResource` - Create a translated version
191
- - `getTranslation(lang: string): Promise<MsgResource>` - Load and apply translations
207
+ - `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
208
  - `getProject(): MsgProject` - Returns the project instance associated with the resource
193
209
  - `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
210
  - `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.");
@@ -124,7 +157,12 @@ var MsgResource = class _MsgResource extends Map {
124
157
  const messages = [];
125
158
  this.forEach((msg) => {
126
159
  if (this.hasMatchingAttributes(msg)) {
127
- messages.push({ key: msg.key, value: msg.value });
160
+ const data = {
161
+ key: msg.key,
162
+ value: msg.value,
163
+ notes: !stripNotes && msg.notes.length > 0 ? msg.notes : void 0
164
+ };
165
+ messages.push(data);
128
166
  } else {
129
167
  messages.push(msg.getData(stripNotes));
130
168
  }
@@ -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.");
@@ -216,7 +249,12 @@ var MsgResource = class _MsgResource extends Map {
216
249
  const messages = [];
217
250
  this.forEach((msg) => {
218
251
  if (this.hasMatchingAttributes(msg)) {
219
- messages.push({ key: msg.key, value: msg.value });
252
+ const data = {
253
+ key: msg.key,
254
+ value: msg.value,
255
+ notes: !stripNotes && msg.notes.length > 0 ? msg.notes : void 0
256
+ };
257
+ messages.push(data);
220
258
  } else {
221
259
  messages.push(msg.getData(stripNotes));
222
260
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MsgResource
3
- } from "../../chunk-7LI2M4JY.mjs";
3
+ } from "../../chunk-2QUOYESW.mjs";
4
4
  import "../../chunk-WUDKNZV2.mjs";
5
5
  import "../../chunk-QWBDIKQK.mjs";
6
6
  export {
@@ -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.");
@@ -218,7 +251,12 @@ var MsgResource = class _MsgResource extends Map {
218
251
  const messages = [];
219
252
  this.forEach((msg) => {
220
253
  if (this.hasMatchingAttributes(msg)) {
221
- messages.push({ key: msg.key, value: msg.value });
254
+ const data = {
255
+ key: msg.key,
256
+ value: msg.value,
257
+ notes: !stripNotes && msg.notes.length > 0 ? msg.notes : void 0
258
+ };
259
+ messages.push(data);
222
260
  } else {
223
261
  messages.push(msg.getData(stripNotes));
224
262
  }
@@ -4,7 +4,7 @@ import {
4
4
  } from "../chunk-XS43NAP2.mjs";
5
5
  import {
6
6
  MsgResource
7
- } from "../chunk-7LI2M4JY.mjs";
7
+ } from "../chunk-2QUOYESW.mjs";
8
8
  import {
9
9
  MsgMessage
10
10
  } from "../chunk-WUDKNZV2.mjs";
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.");
@@ -218,7 +251,12 @@ var MsgResource = class _MsgResource extends Map {
218
251
  const messages = [];
219
252
  this.forEach((msg) => {
220
253
  if (this.hasMatchingAttributes(msg)) {
221
- messages.push({ key: msg.key, value: msg.value });
254
+ const data = {
255
+ key: msg.key,
256
+ value: msg.value,
257
+ notes: !stripNotes && msg.notes.length > 0 ? msg.notes : void 0
258
+ };
259
+ messages.push(data);
222
260
  } else {
223
261
  messages.push(msg.getData(stripNotes));
224
262
  }
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-XS43NAP2.mjs";
5
5
  import {
6
6
  MsgResource
7
- } from "./chunk-7LI2M4JY.mjs";
7
+ } from "./chunk-2QUOYESW.mjs";
8
8
  import {
9
9
  MsgMessage
10
10
  } from "./chunk-WUDKNZV2.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@worldware/msg",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
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",