Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,39 @@ msg create resource myProject messages --edit

**Behavior:**

- Writes the file to `i18n/resources/<title>.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/<title>.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/<projectName>` 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).
Expand Down Expand Up @@ -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).
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
112 changes: 73 additions & 39 deletions src/lib/create-resource-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>` alias from `msg init`.
* @param params - Title, projectName, sourceLocale, dir, and isEsm
* @returns The generated file content
*/
Expand All @@ -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
}
`;
}

Expand Down
30 changes: 26 additions & 4 deletions src/lib/export-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>
): MsgResource | undefined {
const defaultExport = mod.default;
const defaultRecord =
defaultExport && typeof defaultExport === "object"
? (defaultExport as Record<string, unknown>)
: 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";

Expand Down Expand Up @@ -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;
}
Expand Down
Loading
Loading