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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,9 @@ msg export -p myApp

- **Message keys** — Stored as unit `id` (sanitized for XML) and `name` (original key).
- **Resource notes** — Emitted as file-level `<notes>` with category (e.g. `description`, `comment`).
- **Resource attributes** — `dir` → file `srcDir`; `dnt` → file `translate="no"`.
- **Resource attributes** — `dir` → file `srcDir` when it is not the default `"auto"`; `dnt` → file `translate="no"`.
- **Message notes** — Emitted as unit-level `<notes>` with category (e.g. `description`, `context`, `parameters`).
- **Message attributes** — `dnt` → unit `translate="no"`; message `dir` is serialized as the unit’s `srcDir` attribute (XLIFF text direction for the segment).
- **Message attributes** — `dnt` → unit `translate="no"`; message `dir` is serialized as the unit’s `srcDir` unless it is `"auto"`.
- **Message format** — Resolved `format` (`NONE` / `MF1` / `MF2`, including inheritance from resource/project) is written as the unit `type` attribute using the XLIFF custom form `msg:NONE`, `msg:MF1`, or `msg:MF2`.

**Plural, gender, select (PGS):** Classifiable plural/select messages are exported to the [XLIFF 2.2 Plural, Gender, and Select module](https://docs.oasis-open.org/xliff/xliff-core/v2.2/xliff-extended-v2.2-part2.html) (`xmlns:pgs="urn:oasis:names:tc:xliff:pgs:1.0"`): `pgs:switch` on the `<unit>`, and one `<segment>` per variant with `pgs:case`.
Expand Down Expand Up @@ -354,14 +354,14 @@ msg import
# or: msg import --project myApp --language fr
```

**5. Resulting French translation file** — `l10n/translations/myApp/fr/messages.json` (notes omitted; `attributes.dir` is empty unless the XLIFF `<file>` carries `trgDir`):
**5. Resulting French translation file** — `l10n/translations/myApp/fr/messages.json` (notes omitted; `attributes.dir` is `"auto"` unless the XLIFF `<file>` carries `trgDir`):

```json
{
"title": "messages",
"attributes": {
"lang": "fr",
"dir": "",
"dir": "auto",
"dnt": false
},
"messages": [
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.11.0",
"@worldware/msg": "^0.12.0",
"fast-xml-parser": "^5.3.4",
"messageformat": "^4.0.0-10"
},
Expand Down
4 changes: 2 additions & 2 deletions src/commands/create/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export default class CreateProject extends Command {
const loaderPathLine =
"const path = `${TRANSLATION_IMPORT_PATH}/${project}/${language}/${title}.json`;";
const loaderWarnLine =
"console.warn(`Translations for locale ${language} could not be loaded.`, error);";
"console.warn(`Translations for locale ${language} could not be loaded.`);";
const loaderBody = `${loaderPathLine}
try {
const module = await import(path, { with: { type: 'json' } });
Expand All @@ -152,7 +152,7 @@ export default class CreateProject extends Command {
${loaderWarnLine}
return {
title,
attributes: { lang: language, dir: '' },
attributes: { lang: language, dir: 'auto' },
notes: [],
messages: []
};
Expand Down
4 changes: 2 additions & 2 deletions src/lib/export-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ function resourceGroupToXliff22(group: ResourceGroup): string {
`id="${fileId}"`,
`original="${escapeXml(orig)}"`,
];
if (attrs.dir) {
if (attrs.dir && attrs.dir !== "auto") {
fileAttrs.push(`srcDir="${escapeXml(attrs.dir)}"`);
}
if (attrs.dnt === true) {
Expand All @@ -295,7 +295,7 @@ function resourceGroupToXliff22(group: ResourceGroup): string {
if (msgAttr?.dnt === true) {
msgAttrs.push('translate="no"');
}
if (msgAttr?.dir) {
if (msgAttr?.dir && msgAttr.dir !== "auto") {
msgAttrs.push(`srcDir="${escapeXml(msgAttr.dir)}"`);
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/import-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ export function extractResourceFromXliffFile(

const attributes = {
lang: trgLang,
dir: trgDir ?? "",
dir: trgDir ?? "auto",
dnt,
};

Expand Down
6 changes: 4 additions & 2 deletions src/specs/create-project-command.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ const loader = async (project, title, language) => {
const module = await import(path, { with: {type: 'json'}});
return module.default;
} catch (error) {
console.warn(`Translations for locale ${language} could not be loaded.`, error);
console.warn(`Translations for locale ${language} could not be loaded.`);
return {
title,
attributes: {
lang: language,
dir: ''
dir: 'auto'
},
notes: [],
messages: []
Expand Down Expand Up @@ -94,6 +94,8 @@ When retrieving the path for the `i18n` and `l10n` directories from the package.
- It should default `format` to `MF2` when `--format` is omitted and not inherited
- It should inherit `format` from the base project when `--extend` is used and `--format` is omitted
- It should always write `format` on the generated `project` settings object
- It should write `dir: 'auto'` in the loader fallback attributes
- It should warn when translations cannot be loaded without passing the caught error object
- It should write an importable file.

### Constraints
Expand Down
2 changes: 1 addition & 1 deletion src/specs/export-command.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Each unit’s resolved message `format` (`NONE` / `MF1` / `MF2`) is written as `
2. Dynamically import the `MsgResource` objects using the array of file paths, and return an array of `MsgResource` objects.
3. Group the `MsgResource` objects by their associated `MsgProject` project name, and return an array of objects with type `{ project: string, resources: MsgResource[] }`
4. If the `--project [projectName]` filter out the objects where the `project` property does not match `projectName`. Return the filtered array.
5. Serialize each `resources` array to an XLIFF 2.0 string, and return an array of objects with type `{project: string, xliff: string}`
5. Serialize each `resources` array to an XLIFF 2.0 string, and return an array of objects with type `{project: string, xliff: string}`. Omit file/unit `srcDir` when `dir` is the default `"auto"`.
6. Iterate through the array of objects, writing each `xliff` string to a file in the `l10n/xliff` directory that has the `project` name as the filename.

## 7. Implementation
Expand Down
1 change: 1 addition & 0 deletions src/specs/import-command.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Unit `type` values `msg:NONE` / `msg:MF1` / `msg:MF2` (or bare format tokens) ar
- Iterate through the file objects in the parsed xliff object in order to create new translated `MsgResource` objects.
- Use the `original` attribute of the file object to extract the `title` from the file name.
- Use the `trgLang`, `trgDir`, and `translate` attributes to create an object of type `MsgAttributes`.
- Default `dir` to `"auto"` when the file has no `trgDir`.
- If there is `notes` object directly inside the `file` object, use it to create an array of `MsgNote` objects.
- Use `MsgResource.create` with the information just gathered to create a new resource with an empty `messages` array
- Recursively iterate through the file object's `unit` and `group` objects (if any) in order to add messages:
Expand Down
26 changes: 26 additions & 0 deletions src/tests/create-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,32 @@ describe("CreateProject command", () => {
expect(content).not.toContain("export default");
});

test("loader fallback uses dir auto and warns without the error object (CJS)", async () => {
setupValidProject(tmp);
await CreateProject.run(["myApp", "en", "fr"], CLI_ROOT);

const content = readFileSync(join(tmp, "i18n", "projects", "myApp.js"), "utf-8");
expect(content).toContain("dir: 'auto'");
expect(content).not.toContain("dir: ''");
expect(content).toContain(
"console.warn(`Translations for locale ${language} could not be loaded.`);"
);
expect(content).not.toContain("could not be loaded.`, error)");
});

test("loader fallback uses dir auto and warns without the error object (ESM)", async () => {
setupValidProject(tmp, { type: "module" });
await CreateProject.run(["myApp", "en", "fr"], CLI_ROOT);

const content = readFileSync(join(tmp, "i18n", "projects", "myApp.js"), "utf-8");
expect(content).toContain("dir: 'auto'");
expect(content).not.toContain("dir: ''");
expect(content).toContain(
"console.warn(`Translations for locale ${language} could not be loaded.`);"
);
expect(content).not.toContain("could not be loaded.`, error)");
});

test("TypeScript project writes .js file", async () => {
setupValidProject(tmp);
writeFileSync(join(tmp, "tsconfig.json"), JSON.stringify({ compilerOptions: {} }));
Expand Down
23 changes: 23 additions & 0 deletions src/tests/export-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,29 @@ one {{One item}}
expect(result[0].xliff).toContain('srcDir="rtl"');
});

test("omits file srcDir when resource dir is auto", () => {
const res = createTestResource("R", "P", [{ key: "k1", value: "v1" }], {
attributes: { dir: "auto" },
});
const result = serializeResourceGroupsToXliff([
{ project: "P", resources: [res] },
]);
expect(result[0].xliff).not.toContain("srcDir");
});

test("omits unit srcDir when message dir is auto", () => {
const res = createTestResource("R", "P", [
{ key: "k1", value: "v1", attributes: { dir: "auto" } },
], {
attributes: { dir: "ltr" },
});
const xliff = serializeResourceGroupsToXliff([
{ project: "P", resources: [res] },
])[0]!.xliff;
expect(xliff).toContain('srcDir="ltr"');
expect(xliff).not.toMatch(/<unit[^>]*srcDir="auto"/);
});

test("sets unit type from resolved message format", () => {
const res = createTestResource(
"R",
Expand Down
20 changes: 20 additions & 0 deletions src/tests/import-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,26 @@ one {{一}}
expect(data.notes![0].content).toBe("File note");
});

test("defaults resource dir to auto when file has no trgDir", () => {
const fileEl = {
"@_original": "R.json",
"@_trgLang": "zh",
unit: {
"@_id": "u1",
"@_name": "k1",
segment: { source: "S", target: "T" },
},
};
const result = extractResourceFromXliffFile(
fileEl as unknown as Record<string, unknown>,
"zh",
project,
["zh"]
);
expect(result).toBeInstanceOf(MsgResource);
expect(result!.attributes.dir).toBe("auto");
});

test("extracts from groups (nested structure)", () => {
const fileEl = {
"@_original": "Grouped.json",
Expand Down
Loading