diff --git a/README.md b/README.md
index 2ebb8de..743d9de 100644
--- a/README.md
+++ b/README.md
@@ -154,12 +154,39 @@ msg create resource myProject messages --edit
**Behavior:**
-- Writes the file to `i18n/resources/
.msg.js` (always `.js`).
-- Uses ES module or CommonJS export syntax based on `package.json` `"type"` or presence of `tsconfig.json`.
+- Writes the file to `i18n/resources/.msg.js` (always `.js`, including TypeScript projects).
+- Uses ES module or CommonJS syntax based on `package.json` `"type"` or presence of `tsconfig.json`.
+- Imports the project via the `#i18n/projects/` alias (added by `msg init`).
- Sets `lang` from the project's `sourceLocale` and `dir` to `rtl` for Arabic/Hebrew, `ltr` otherwise.
-- Includes a minimal example message. Validates that the generated file is importable.
+- Exports a named `resource` plus an async `getMessages()` helper that returns `resource.getTranslation(getLang())`, so the resource can be pre-translated for the runtime locale.
+- Includes sample messages added via chainable `.add()`. Validates that the generated file is importable.
- Errors if i18n/projects or i18n/resources does not exist, the project is not found, or the resource file already exists (unless `--force`).
+**Generated file (ESM excerpt):**
+
+```javascript
+import { MsgResource, getLang } from '@worldware/msg';
+import project from '#i18n/projects/myProject';
+
+export const resource = MsgResource.create({ /* title, attributes, notes */ }, project);
+
+resource
+ .add('sampleKey', 'Sample value.', {}, [/* notes */])
+ .add('sampleKey2', 'Hi, {name}', { dnt: true }, [/* notes */]);
+
+export async function getMessages() {
+ return await resource.getTranslation(getLang());
+}
+```
+
+**Usage:**
+
+```javascript
+import { getMessages } from '#i18n/resources/messages.msg.js';
+
+const messages = await getMessages();
+```
+
### export
Serialize all MsgResource files in `i18n/resources` to XLIFF 2.2 files in `l10n/xliff`, one file per project. Does not send files for translation; use your own translation workflow with the generated XLIFF. Requires `package.json` with `directories.i18n` and `directories.l10n` (run `msg init` first).
@@ -187,7 +214,7 @@ msg export -p myApp
**Behavior:**
- Recursively finds all `.msg.js` and `.msg.ts` files under `i18n/resources`.
-- Imports each file as a MsgResource; errors if any file is invalid.
+- Imports each file as a MsgResource (default export, or named `resource` / `MsgResource`, including CJS `module.exports = { resource }`); errors if any file is invalid.
- Groups resources by project name and writes one XLIFF 2.2 file per project to `l10n/xliff` (e.g. `myApp.xliff`).
- With `--project`, only that project is exported; existing other files in `l10n/xliff` are not removed.
- If no MsgResource files are found, exits with an informational message (no error).
diff --git a/package-lock.json b/package-lock.json
index f8b336e..79920cb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,7 @@
"@messageformat/icu-messageformat-1": "^0.12.0",
"@messageformat/parser": "^5.1.1",
"@oclif/core": "^3.21.2",
- "@worldware/msg": "^0.8.1",
+ "@worldware/msg": "^0.10.0",
"fast-xml-parser": "^5.3.4",
"messageformat": "^4.0.0-10"
},
@@ -1943,9 +1943,9 @@
}
},
"node_modules/@worldware/msg": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/@worldware/msg/-/msg-0.8.1.tgz",
- "integrity": "sha512-7c0YTYSLCXIrCgAF/gl/3hwNwQ95Dqb8olQuwqbrQmkyP89K+yUtWJSNRPwLP6Um5oOmxk86D9C4xBHI0Vc/hQ==",
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@worldware/msg/-/msg-0.10.0.tgz",
+ "integrity": "sha512-XPtA3h0dT0SkTF6llxkHf7bQ9xSBYNpYfeXM8eMNN/KJbAIxjD0pev71ahG8h++NwYpBje2YgdhZXWLwG+rf4w==",
"license": "MIT",
"dependencies": {
"@messageformat/icu-messageformat-1": "^0.12.0",
diff --git a/package.json b/package.json
index 5ae52e6..0700060 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
"@messageformat/icu-messageformat-1": "^0.12.0",
"@messageformat/parser": "^5.1.1",
"@oclif/core": "^3.21.2",
- "@worldware/msg": "^0.8.1",
+ "@worldware/msg": "^0.10.0",
"fast-xml-parser": "^5.3.4",
"messageformat": "^4.0.0-10"
},
diff --git a/src/lib/create-resource-helpers.ts b/src/lib/create-resource-helpers.ts
index 668fd32..6fb985a 100644
--- a/src/lib/create-resource-helpers.ts
+++ b/src/lib/create-resource-helpers.ts
@@ -114,8 +114,19 @@ export async function importMsgProjectForResource(
return undefined;
}
+/**
+ * Escapes a string for use inside a single-quoted JS literal.
+ */
+function escapeSingleQuoted(value: string): string {
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
+}
+
/**
* Generates the MsgResource file content as a string.
+ * Emits ESM or CJS boilerplate that creates a MsgResource, adds sample
+ * messages via chainable `.add()`, and exports `resource` plus an async
+ * `getMessages` loader that calls `resource.getTranslation(getLang())`.
+ * Project import uses the `#i18n/projects/` alias from `msg init`.
* @param params - Title, projectName, sourceLocale, dir, and isEsm
* @returns The generated file content
*/
@@ -127,55 +138,78 @@ export function generateMsgResourceContent(params: {
isEsm: boolean;
}): string {
const { title, projectName, sourceLocale, dir, isEsm } = params;
- const projectImport = isEsm
- ? `../projects/${projectName}.js`
- : `../projects/${projectName}`;
- const messagesBlock = ` messages: [
- {
- key: 'example.message',
- value: 'Example message.',
- notes: [
- { type: 'DESCRIPTION', content: 'This is an example message. You can delete it.' }
- ]
- }
- ]`;
-
- const titleStr = `'${title.replace(/'/g, "\\'")}'`;
- const langStr = `'${sourceLocale.replace(/'/g, "\\'")}'`;
+ const titleStr = `'${escapeSingleQuoted(title)}'`;
+ const langStr = `'${escapeSingleQuoted(sourceLocale)}'`;
const dirStr = `'${dir}'`;
+ const resourceNote = `This is the ${escapeSingleQuoted(title)} resource.`;
+ const projectImport = `#i18n/projects/${escapeSingleQuoted(projectName)}`;
+
+ const createAndAdd = `MsgResource.create({
+ title: ${titleStr},
+ attributes: {
+ lang: ${langStr},
+ dir: ${dirStr}
+ },
+ notes: [
+ {type: 'DESCRIPTION', content: '${resourceNote}'}
+ ]
+ }, project);
+
+/**
+ * Add messages to the resource using add(key, value, attributes, notes)
+ * The add method is chainable.
+ */
+
+resource
+ .add('sampleKey', 'Sample value.', {}, [
+ { type: 'DESCRIPTION', content: 'This is first message.' }
+ ])
+ .add('sampleKey2', 'Hi, {name}', { dnt: true }, [
+ { type: 'DESCRIPTION', content: 'This is the second message.' },
+ { type: 'PARAMETERS', content: 'The {name} parameter holds the user name.' }
+ ]);`;
if (isEsm) {
- return `import { MsgResource } from '@worldware/msg';
+ return `/** ESM module **/
+
+import { MsgResource, getLang } from '@worldware/msg';
import project from '${projectImport}';
-export default MsgResource.create({
- title: ${titleStr},
- attributes: {
- lang: ${langStr},
- dir: ${dirStr}
- },
- notes: [
- { type: 'DESCRIPTION', content: 'This is a generated file. Replace this description with your own.' }
- ],
-${messagesBlock}
-}, project);
+/** Create a MsgResource object */
+
+export const resource = ${createAndAdd}
+
+/**
+ * An async function to get a translated version of the resource
+ * If the runtime language has not been set using \`setLang()\`,
+ * it will return the original resource
+ */
+export async function getMessages() {
+ return await resource.getTranslation(getLang());
+}
`;
}
- return `const { MsgResource } = require('@worldware/msg');
+ return `/** CJS implementation **/
+
+const { MsgResource, getLang } = require('@worldware/msg');
const project = require('${projectImport}');
-module.exports = MsgResource.create({
- title: ${titleStr},
- attributes: {
- lang: ${langStr},
- dir: ${dirStr}
- },
- notes: [
- { type: 'DESCRIPTION', content: 'This is a generated file. Replace this description with your own.' }
- ],
-${messagesBlock}
-}, project);
+const resource = ${createAndAdd}
+
+/**
+ * An async function to get a translated version of the resource
+ * If a runtime language has not been set using \`setLang()\`,
+ * it will return the original resource
+ */
+async function getMessages() {
+ return await resource.getTranslation(getLang());
+}
+
+module.exports = {
+ resource,
+ getMessages
+}
`;
}
diff --git a/src/lib/export-helpers.ts b/src/lib/export-helpers.ts
index ab9c8b8..33320d1 100644
--- a/src/lib/export-helpers.ts
+++ b/src/lib/export-helpers.ts
@@ -36,6 +36,29 @@ function isMsgResourceLike(value: unknown): value is MsgResource {
);
}
+/**
+ * Picks a MsgResource from a dynamically imported module.
+ * Supports default export, named `resource` / `MsgResource`, and CJS
+ * `module.exports = { resource }` (where `default` is the exports object).
+ */
+function resolveMsgResourceExport(
+ mod: Record
+): MsgResource | undefined {
+ const defaultExport = mod.default;
+ const defaultRecord =
+ defaultExport && typeof defaultExport === "object"
+ ? (defaultExport as Record)
+ : undefined;
+ const candidates: unknown[] = [
+ defaultExport,
+ mod.resource,
+ mod.MsgResource,
+ defaultRecord?.resource,
+ defaultRecord?.MsgResource,
+ ];
+ return candidates.find(isMsgResourceLike);
+}
+
const XLIFF22_NS = "urn:oasis:names:tc:xliff:document:2.2";
const PGS_NS = "urn:oasis:names:tc:xliff:pgs:1.0";
@@ -78,14 +101,13 @@ export async function importMsgResourcesFromPaths(
}
const url = pathToFileURL(filePath).href;
const mod = await dynamicImportFromUrl(url);
- const resource: unknown =
- mod.default ?? (mod as { resource?: unknown }).resource ?? (mod as { MsgResource?: unknown }).MsgResource;
- if (!isMsgResourceLike(resource)) {
+ const resource = resolveMsgResourceExport(mod);
+ if (!resource) {
throw new Error(
`Failed to import MsgResource from ${filePath}: no valid export found`
);
}
- result.push(resource as MsgResource);
+ result.push(resource);
}
return result;
}
diff --git a/src/specs/create-resource-command.spec.md b/src/specs/create-resource-command.spec.md
index 49c510a..fe55bfa 100644
--- a/src/specs/create-resource-command.spec.md
+++ b/src/specs/create-resource-command.spec.md
@@ -1,34 +1,57 @@
## 1. Summary
-The `create resource` command creates a `MsgResource` file in the `resources` subdirectory of the `i18n` created by the `init` command. Running the `init` command is a prerequisite to running the `create resource` command. An `MsgResource` file is a javascript or typescript file that exports a `MsgResource` instance. These files have `.msg.` right before the file extension, and are named after the resource `title`. For example, `messages.msg.js`. The `create resource` command should create a file that exports a minimal `MsgResource` instance, using the `projectName` and `title` arguments passed to the command. The `projectName` argument is used to reference the project file in the `i18n/projects` subdirectory. Project files use the project name as the file name, so the correct file to reference in the `MsgResource` import can be easily determined. The generated `MsgResource` file should look like approximately like this:
+The `create resource` command creates a `MsgResource` file in the `resources` subdirectory of the `i18n` created by the `init` command. Running the `init` command is a prerequisite to running the `create resource` command. An `MsgResource` file is a JavaScript file that exports a `MsgResource` instance (and a `getMessages` loader). These files have `.msg.` right before the `.js` extension, and are named after the resource `title`. For example, `messages.msg.js`. The command always writes a `.js` file, even when the surrounding project uses TypeScript.
+
+The generated file uses the `projectName` and `title` arguments, imports the project via the `#i18n/projects/` alias (configured by `msg init`), and scaffolds sample messages plus an async `getMessages()` helper that calls `resource.getTranslation(getLang())` so callers can load a pre-translated resource for the runtime locale. An ESM example:
```javascript
-import { MsgResource } from '@worldware/msg';
-import project from '../projects/';
-
-export default MsgResource.create({
- title: ,
- attributes: {
- lang: ,
- dir: <'rtl' or 'ltr'>
- },
- notes: [
- { type: 'DESCRIPTION', content: 'This is a generated file. Replace this description with your own.'}
- ],
- messages: [
- {
- key: 'example.message',
- value: 'Example message.'
- notes: [
- { type: 'DESCRIPTION', content 'This is an example message. You can delete it.' }
- ]
- }
- ]
-}, project);
+/** ESM module **/
+
+import { MsgResource, getLang } from '@worldware/msg';
+import project from '#i18n/projects/';
+
+/** Create a MsgResource object */
+
+export const resource = MsgResource.create({
+ title: '',
+ attributes: {
+ lang: '',
+ dir: "<'rtl' or 'ltr'>"
+ },
+ notes: [
+ {type: 'DESCRIPTION', content: 'This is the resource.'}
+ ]
+ }, project);
+
+/**
+ * Add messages to the resource using add(key, value, attributes, notes)
+ * The add method is chainable.
+ */
+
+resource
+ .add('sampleKey', 'Sample value.', {}, [
+ { type: 'DESCRIPTION', content: 'This is first message.' }
+ ])
+ .add('sampleKey2', 'Hi, {name}', { dnt: true }, [
+ { type: 'DESCRIPTION', content: 'This is the second message.' },
+ { type: 'PARAMETERS', content: 'The {name} parameter holds the user name.' }
+ ]);
+
+/**
+ * An async function to get a translated version of the resource
+ * If the runtime language has not been set using `setLang()`,
+ * it will return the original resource
+ */
+export async function getMessages() {
+ return await resource.getTranslation(getLang());
+}
```
-The terms in angle brackets `<>` above are variables to be replaced with the actual values. If the sourceLocale uses `ar` or `he` as the language subtag, set `dir` to `'rtl'`. Otherwise, it should be set to `''ltr'` by default.
-The resulting file should be importable either as the default export of an ES module or as the main export of a CommonJS module. Which module format is used should depend on the module type being used in the surrounding project. If the project is using typescript, create a typescript file, but the content of that file would be no different from a javascript file.
+CommonJS projects get the same structure with `require` / `module.exports = { resource, getMessages }`.
+
+The terms in angle brackets `<>` above are variables to be replaced with the actual values. If the sourceLocale uses `ar` or `he` as the language subtag, set `dir` to `'rtl'`. Otherwise, it should be set to `'ltr'` by default.
+
+Which module format is used depends on the surrounding project (`package.json` `"type": "module"` or TypeScript → ESM; otherwise CJS).
The general order of operations for the command happy path should be as follows:
@@ -37,7 +60,7 @@ The general order of operations for the command happy path should be as follows:
3. Import the project file associated with the `projectName` argument from the `i18n/projects` directory.
4. Retrieve the sourceLocale from the project `locales` settings.
5. Create a template string based on the code above that inserts the necessary variable values.
-6. Write the string to file in the `i18n/resources` directory, using the pattern: `.msg.`
+6. Write the string to file in the `i18n/resources` directory, using the pattern: `.msg.js`
## 2. Context
@@ -49,6 +72,7 @@ The general order of operations for the command happy path should be as follows:
- As a `Application Developer`, I want `to quickly scaffold a resource file`, so that `I do not have to do it myself`.
- As a `Application Developer`, I want `the option to automatically open the file`, so that `I can quickly start editing it`.
+- As a `Application Developer`, I want `the scaffolded resource to expose getMessages()`, so that `I can load a pre-translated resource for the runtime locale`.
## 3. Functionality
@@ -56,6 +80,7 @@ The general order of operations for the command happy path should be as follows:
- It `creates a MsgResource file in the i18n/resources directory` in order to `make it easy to start defining a MsgResource`.
- It `associates every MsgResource with a MsgProject` in order to `pass on the project configuration to the resource and facilitate export to xliff`.
+- It `exports resource and getMessages` in order to `support pre-translated loading via getLang()/getTranslation()`.
### Secondary Functions
@@ -63,9 +88,9 @@ The general order of operations for the command happy path should be as follows:
- It `imports the package.json file and gets the module type` in order to `determine what module type to use for the generated file`
- It `retrieves the sourceLocale from the MsgProject instance` in order to `set the language on the MsgResource file`
- It `tries to calculate the base direction (dir) based on the language subtag` in order to `set the direction on the MsgResource file`
-- It `creates a template string for a minimal MsgResource file` in order to `produce the content for the MsgResource file`
+- It `creates a template string for a MsgResource file with sample .add() messages and getMessages` in order to `produce the content for the MsgResource file`
- It `determines the module type of the surrounding project` in order to `produce the correct export syntax for the generated content`
-- It `writes the generated content to a file named after the title followed .msg.` in order to `persist the MsgResource`
+- It `writes the generated content to a file named .msg.js` in order to `persist the MsgResource`
## 4. Behavior
@@ -75,8 +100,10 @@ The general order of operations for the command happy path should be as follows:
- It `must` throw an error if the file cannot be generated.
- It `must` validate that the file is valid and importable, throwing an error if it is not.
- It `must` be able to work on different platforms.
-- It `must` name the generated file using the `title` argument.
-- It `should` produce a typescript file if a `tsconfig.json` file is present.
+- It `must` name the generated file using the `title` argument with a `.msg.js` suffix.
+- It `must` always write a `.js` file (not `.ts`), even when `tsconfig.json` is present.
+- It `must` import the project via `#i18n/projects/`.
+- It `must` export a named `resource` and an async `getMessages()` loader.
- It `should` error if the `i18n/projects` or `i18n/resources` directories do not exist and prompt to run the `init` command.
- It `should` error if the `projectName` or `title` arguments are not provided.
- It `should` provide an option to open the MsgResource file after it is created.
@@ -212,18 +239,17 @@ The general order of operations for the command happy path should be as follows:
- _[Create resource in ES module project]_
- Given: A project with `init` run, `package.json` with `"type": "module"`, and a project file `i18n/projects/myProject.js`
- When: User runs `create resource myProject messages`
- - Then: A file `i18n/resources/messages.msg.js` is created with valid MsgResource content, correct `title`, import from `../projects/myProject`, and default `dir: 'ltr'` for non-RTL sourceLocale.
+ - Then: A file `i18n/resources/messages.msg.js` is created with valid MsgResource content, correct `title`, import from `#i18n/projects/myProject`, named `resource` export, `getMessages()`, and default `dir: 'ltr'` for non-RTL sourceLocale.
- _[Create resource in CommonJS project]_
- Given: A project with `init` run, `package.json` without `"type": "module"` (or `"type": "commonjs"`), and a project file in `i18n/projects`
- When: User runs `create resource myProject messages`
- - Then: A file `i18n/resources/messages.msg.cjs` (or appropriate CJS extension) is created with CommonJS-compatible export and valid MsgResource content.
+ - Then: A file `i18n/resources/messages.msg.js` is created with CommonJS `module.exports = { resource, getMessages }` and valid MsgResource content.
-- _[Create resource produces TypeScript when tsconfig present]_
+- _[Create resource always writes .js even when tsconfig present]_
- Given: A project with `init` run, `tsconfig.json` at project root, and a project file in `i18n/projects`
- When: User runs `create resource myProject messages`
- - Then: A file `i18n/resources/messages.msg.ts` is created with valid MsgResource content and correct TypeScript syntax.
-
+ - Then: A file `i18n/resources/messages.msg.js` is created (not `.ts`) with ESM-style `resource` and `getMessages` exports.
- _[Create resource sets dir to RTL for Arabic sourceLocale]_
- Given: A project with `init` run and a project file whose `locales.sourceLocale` is `ar` or `ar-*`
- When: User runs `create resource myProject messages`
@@ -252,19 +278,19 @@ The general order of operations for the command happy path should be as follows:
- _[Generated file is valid and importable]_
- Given: A project with `init` run and a project file in `i18n/projects`
- When: User runs `create resource myProject messages`
- - Then: The written file can be imported as default export (ES) or main export (CJS) and exports a valid MsgResource instance.
+ - Then: The written file exports a named `resource` (ESM `export const resource` or CJS `module.exports.resource`) plus `getMessages`, and `resource` is a valid MsgResource instance.
#### Edge Cases
- _[Title used as filename with safe naming]_
- Given: A project with `init` run and a project file in `i18n/projects`
- When: User runs `create resource myProject my-messages` or `create resource myProject my_messages`
- - Then: The file is created as `i18n/resources/my-messages.msg.` (or `my_messages.msg.`) with content `title: 'my-messages'` (or matching title).
+ - Then: The file is created as `i18n/resources/my-messages.msg.js` (or `my_messages.msg.js`) with content `title: 'my-messages'` (or matching title) and loader still named `getMessages`.
- _[Project name matches exactly one project file]_
- Given: `i18n/projects/app.js` and `i18n/projects/other.js` exist
- When: User runs `create resource app dashboard`
- - Then: The generated file imports from `../projects/app` and no ambiguity error occurs.
+ - Then: The generated file imports from `#i18n/projects/app` and no ambiguity error occurs.
- _[Source locale with compound tag still drives RTL]_
- Given: A project file with `locales.sourceLocale` set to `ar-SA` or `he-IL`
@@ -279,7 +305,7 @@ The general order of operations for the command happy path should be as follows:
- _[Short and minimal projectName and title]_
- Given: A project with `init` run
- When: User runs `create resource p t`
- - Then: A file `i18n/resources/t.msg.` is created with `title: 't'` and project import from `../projects/p`, and the file is valid.
+ - Then: A file `i18n/resources/t.msg.js` is created with `title: 't'` and project import from `#i18n/projects/p`, and the file is valid.
#### Errors
diff --git a/src/tests/create-resource-helpers.test.ts b/src/tests/create-resource-helpers.test.ts
index e8735da..ffc2402 100644
--- a/src/tests/create-resource-helpers.test.ts
+++ b/src/tests/create-resource-helpers.test.ts
@@ -181,37 +181,44 @@ describe("create-resource-helpers", () => {
});
describe("generateMsgResourceContent", () => {
- test("generates ESM content with correct structure", () => {
+ test("generates ESM content with getLang, named resource, and loader", () => {
const content = generateMsgResourceContent({
- title: "messages",
- projectName: "myProject",
+ title: "Messages",
+ projectName: "Main",
sourceLocale: "en",
dir: "ltr",
isEsm: true,
});
- expect(content).toContain("import { MsgResource } from '@worldware/msg'");
- expect(content).toContain("import project from '../projects/myProject.js'");
- expect(content).toContain("title: 'messages'");
+ expect(content).toContain("import { MsgResource, getLang } from '@worldware/msg'");
+ expect(content).toContain("import project from '#i18n/projects/Main'");
+ expect(content).toContain("title: 'Messages'");
expect(content).toContain("lang: 'en'");
expect(content).toContain("dir: 'ltr'");
- expect(content).toContain("export default MsgResource.create");
+ expect(content).toContain("export const resource = MsgResource.create");
+ expect(content).toContain(".add('sampleKey'");
+ expect(content).toContain(".add('sampleKey2'");
+ expect(content).toContain("export async function getMessages()");
+ expect(content).toContain("resource.getTranslation(getLang())");
+ expect(content).not.toContain("export default");
expect(content).not.toContain("module.exports");
- expect(content).toContain("example.message");
- expect(content).toContain("Example message.");
+ expect(content).not.toContain("../projects/");
});
- test("generates CJS content with module.exports", () => {
+ test("generates CJS content with named exports object", () => {
const content = generateMsgResourceContent({
- title: "messages",
- projectName: "myProject",
+ title: "Messages",
+ projectName: "Main",
sourceLocale: "en",
dir: "ltr",
isEsm: false,
});
- expect(content).toContain("require('@worldware/msg')");
- expect(content).toContain("require('../projects/myProject')");
- expect(content).toContain("module.exports = MsgResource.create");
+ expect(content).toContain("const { MsgResource, getLang } = require('@worldware/msg')");
+ expect(content).toContain("require('#i18n/projects/Main')");
+ expect(content).toContain("const resource = MsgResource.create");
+ expect(content).toContain("async function getMessages()");
+ expect(content).toContain("module.exports = {\n resource,\n getMessages\n}");
expect(content).not.toContain("export default");
+ expect(content).not.toContain("export const resource");
expect(content).toContain("dir: 'ltr'");
});
@@ -226,7 +233,7 @@ describe("create-resource-helpers", () => {
expect(content).toContain("dir: 'rtl'");
});
- test("handles title with hyphens", () => {
+ test("always names the loader getMessages regardless of title", () => {
const content = generateMsgResourceContent({
title: "my-messages",
projectName: "app",
@@ -235,6 +242,7 @@ describe("create-resource-helpers", () => {
isEsm: true,
});
expect(content).toContain("title: 'my-messages'");
+ expect(content).toContain("export async function getMessages()");
});
test("handles short projectName and title", () => {
@@ -246,7 +254,8 @@ describe("create-resource-helpers", () => {
isEsm: true,
});
expect(content).toContain("title: 't'");
- expect(content).toContain("../projects/p.js");
+ expect(content).toContain("import project from '#i18n/projects/p'");
+ expect(content).toContain("export async function getMessages()");
});
test("escapes single quotes in title", () => {
@@ -261,6 +270,17 @@ describe("create-resource-helpers", () => {
expect(content).not.toMatch(/title: 'O'Brien'/);
});
+ test("escapes single quotes in projectName", () => {
+ const content = generateMsgResourceContent({
+ title: "messages",
+ projectName: "O'Brien",
+ sourceLocale: "en",
+ dir: "ltr",
+ isEsm: true,
+ });
+ expect(content).toContain("import project from '#i18n/projects/O\\'Brien'");
+ });
+
test("escapes single quotes in sourceLocale", () => {
const content = generateMsgResourceContent({
title: "messages",
@@ -271,6 +291,19 @@ describe("create-resource-helpers", () => {
});
expect(content).toMatch(/lang:\s*'[^']*'/);
});
+
+ test("includes resource DESCRIPTION note derived from title", () => {
+ const content = generateMsgResourceContent({
+ title: "Messages",
+ projectName: "Main",
+ sourceLocale: "en",
+ dir: "ltr",
+ isEsm: true,
+ });
+ expect(content).toContain(
+ "{type: 'DESCRIPTION', content: 'This is the Messages resource.'}"
+ );
+ });
});
describe("writeMsgResourceFile", () => {
diff --git a/src/tests/create-resource.test.ts b/src/tests/create-resource.test.ts
index 1195f70..ddef2df 100644
--- a/src/tests/create-resource.test.ts
+++ b/src/tests/create-resource.test.ts
@@ -28,14 +28,26 @@ function setupValidProject(
projectContent?: string,
skipProjectFile?: boolean
) {
+ const directories = {
+ i18n: "i18n",
+ l10n: "l10n",
+ root: ".",
+ ...((pkgOverrides.directories as Record | undefined) ?? {}),
+ };
const pkg = {
name: "test-app",
version: "1.0.0",
- directories: { i18n: "i18n", l10n: "l10n", root: "." },
+ directories,
+ imports: {
+ "#i18n/*": `./${directories.i18n}/*`,
+ "#l10n/*": `./${directories.l10n}/*`,
+ "#root/*": "./*",
+ },
...pkgOverrides,
+ directories,
};
writeFileSync(join(tmp, "package.json"), JSON.stringify(pkg, null, 2));
- const i18nDir = join(tmp, (pkg.directories as { i18n: string }).i18n);
+ const i18nDir = join(tmp, directories.i18n);
const projectsDir = join(i18nDir, "projects");
const resourcesDir = join(i18nDir, "resources");
for (const d of [projectsDir, resourcesDir]) {
@@ -90,11 +102,13 @@ describe("CreateResource command", () => {
const outPath = join(tmp, "i18n", "resources", "messages.msg.js");
expect(existsSync(outPath)).toBe(true);
const content = readFileSync(outPath, "utf-8");
- expect(content).toContain("import { MsgResource } from '@worldware/msg'");
- expect(content).toContain("import project from '../projects/myProject.js'");
+ expect(content).toContain("import { MsgResource, getLang } from '@worldware/msg'");
+ expect(content).toContain("import project from '#i18n/projects/myProject'");
expect(content).toContain("title: 'messages'");
expect(content).toContain("dir: 'ltr'");
- expect(content).toContain("export default MsgResource.create");
+ expect(content).toContain("export const resource = MsgResource.create");
+ expect(content).toContain("export async function getMessages()");
+ expect(content).not.toContain("export default");
});
test("creates resource file in CommonJS project", async () => {
@@ -102,10 +116,12 @@ describe("CreateResource command", () => {
await CreateResource.run(["myProject", "messages"], CLI_ROOT);
const content = readFileSync(join(tmp, "i18n", "resources", "messages.msg.js"), "utf-8");
- expect(content).toContain("require('@worldware/msg')");
- expect(content).toContain("require('../projects/myProject')");
- expect(content).toContain("module.exports = MsgResource.create");
+ expect(content).toContain("const { MsgResource, getLang } = require('@worldware/msg')");
+ expect(content).toContain("require('#i18n/projects/myProject')");
+ expect(content).toContain("async function getMessages()");
+ expect(content).toContain("module.exports = {\n resource,\n getMessages\n}");
expect(content).toContain("dir: 'ltr'");
+ expect(content).not.toContain("export default");
});
test("produces JavaScript file with ESM when tsconfig present", async () => {
@@ -116,8 +132,9 @@ describe("CreateResource command", () => {
expect(existsSync(join(tmp, "i18n", "resources", "messages.msg.js"))).toBe(true);
expect(existsSync(join(tmp, "i18n", "resources", "messages.msg.ts"))).toBe(false);
const content = readFileSync(join(tmp, "i18n", "resources", "messages.msg.js"), "utf-8");
- expect(content).toContain("import { MsgResource } from '@worldware/msg'");
- expect(content).toContain("export default MsgResource.create");
+ expect(content).toContain("import { MsgResource, getLang } from '@worldware/msg'");
+ expect(content).toContain("export const resource = MsgResource.create");
+ expect(content).toContain("export async function getMessages()");
expect(content).not.toContain("module.exports");
});
@@ -176,6 +193,8 @@ describe("CreateResource command", () => {
expect(existsSync(outPath)).toBe(true);
const content = readFileSync(outPath, "utf-8");
expect(content).toContain("MsgResource.create");
+ expect(content).toContain("getLang");
+ expect(content).toContain("getMessages");
expect(content).toContain("title:");
expect(content).toContain("messages");
});
@@ -189,7 +208,7 @@ describe("CreateResource command", () => {
const outPath = join(tmp, "i18n", "resources", "messages.msg.js");
expect(existsSync(outPath)).toBe(true);
const content = readFileSync(outPath, "utf-8");
- expect(content).toContain("import project from '../projects/myApp.js'");
+ expect(content).toContain("import project from '#i18n/projects/myApp'");
expect(content).toMatch(/lang:\s*['\"]en['\"]/);
expect(content).toMatch(/dir:\s*['\"]ltr['\"]/);
});
@@ -203,6 +222,7 @@ describe("CreateResource command", () => {
expect(existsSync(join(tmp, "i18n", "resources", "my-messages.msg.js"))).toBe(true);
const content = readFileSync(join(tmp, "i18n", "resources", "my-messages.msg.js"), "utf-8");
expect(content).toContain("title: 'my-messages'");
+ expect(content).toContain("getMessages");
});
test("short projectName and title", async () => {
@@ -218,7 +238,8 @@ describe("CreateResource command", () => {
expect(existsSync(join(tmp, "i18n", "resources", "t.msg.js"))).toBe(true);
const content = readFileSync(join(tmp, "i18n", "resources", "t.msg.js"), "utf-8");
expect(content).toContain("title: 't'");
- expect(content).toContain("../projects/p");
+ expect(content).toContain("#i18n/projects/p");
+ expect(content).toContain("getMessages");
});
test("custom i18n path", async () => {
@@ -236,6 +257,11 @@ describe("CreateResource command", () => {
await CreateResource.run(["myProject", "messages"], CLI_ROOT);
expect(existsSync(join(tmp, "lib", "i18n", "resources", "messages.msg.js"))).toBe(true);
+ const content = readFileSync(
+ join(tmp, "lib", "i18n", "resources", "messages.msg.js"),
+ "utf-8"
+ );
+ expect(content).toContain("#i18n/projects/myProject");
});
});
diff --git a/src/tests/export-helpers.test.ts b/src/tests/export-helpers.test.ts
index 9434d91..90a9e4d 100644
--- a/src/tests/export-helpers.test.ts
+++ b/src/tests/export-helpers.test.ts
@@ -166,6 +166,75 @@ describe("export-helpers", () => {
const result = await importMsgResourcesFromPaths(twoPaths);
expect(result).toHaveLength(2);
});
+
+ test("imports MsgResource from named resource export (create resource shape)", async () => {
+ const tmp = join(tmpdir(), `msg-named-resource-${Date.now()}`);
+ mkdirSync(tmp, { recursive: true });
+ const cliRoot = join(__dirname, "..", "..");
+ try {
+ const { symlinkSync } = await import("fs");
+ try {
+ const target = join(cliRoot, "node_modules");
+ if (existsSync(target)) {
+ symlinkSync(target, join(tmp, "node_modules"), "dir");
+ }
+ } catch {
+ // Skip if symlink fails (e.g. sandbox)
+ }
+
+ writeFileSync(
+ join(tmp, "Named.msg.js"),
+ `const { MsgProject, MsgResource, getLang } = require('@worldware/msg');
+const project = MsgProject.create({
+ project: { name: 'namedProj' },
+ locales: {
+ sourceLocale: 'en',
+ pseudoLocale: 'en-XA',
+ targetLocales: { en: ['en'] },
+ },
+ loader: async () => ({
+ title: '',
+ attributes: { lang: '', dir: '', dnt: false },
+ messages: [],
+ }),
+});
+const resource = MsgResource.create({
+ title: 'Named',
+ attributes: { lang: 'en', dir: 'ltr' },
+ notes: [{ type: 'DESCRIPTION', content: 'This is the Named resource.' }],
+}, project);
+resource.add('sampleKey', 'Sample value.', {}, [
+ { type: 'DESCRIPTION', content: 'This is first message.' },
+]);
+async function getMessages() {
+ return await resource.getTranslation(getLang());
+}
+module.exports = { resource, getMessages };
+`,
+ "utf-8"
+ );
+
+ const result = await importMsgResourcesFromPaths([
+ join(tmp, "Named.msg.js"),
+ ]);
+ expect(result).toHaveLength(1);
+ expect(result[0].title).toBe("Named");
+ expect(result[0].getProject().project.name).toBe("namedProj");
+ expect(
+ result[0].getData().messages?.some((m) => m.key === "sampleKey")
+ ).toBe(true);
+
+ const { createRequire } = await import("module");
+ const req = createRequire(join(tmp, "Named.msg.js"));
+ const mod = req(join(tmp, "Named.msg.js")) as {
+ getMessages: () => Promise;
+ };
+ const translated = await mod.getMessages();
+ expect(translated.title).toBe("Named");
+ } finally {
+ rmSync(tmp, { recursive: true, force: true });
+ }
+ });
});
describe("groupResourcesByProject", () => {