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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const loader = async (project, title, language) => {
console.warn(`Translations for locale ${language} could not be loaded.`, error);
return {
title,
attributes: { lang: language, dir: '' },
attributes: { lang: language, dir: 'auto' },
notes: [],
messages: []
};
Expand Down Expand Up @@ -332,7 +332,7 @@ const data = resource.getData();
### Types

- `MsgFormat` - `'MF1' | 'MF2' | 'NONE'`; the formatting syntax for a message.
- `MsgAttributes` - `{ lang?: string; dir?: string; dnt?: boolean; format?: MsgFormat }`.
- `MsgAttributes` - `{ lang?: string; dir?: string; dnt?: boolean; format?: MsgFormat }`. Omitted or empty `dir` resolves to `'auto'`.

### Pseudo-localization helpers

Expand Down
2 changes: 1 addition & 1 deletion src/classes/MsgInterface/MsgInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const MSG_DEFAULT_FORMAT: MsgFormat = 'MF2';
export type MsgAttributes = {
/** BCP 47 language tag for the content (for example, `en` or `zh-Hans`). */
lang?: string
/** Text direction: typically `ltr`, `rtl`, or `auto`. */
/** Text direction: typically `ltr`, `rtl`, or `auto`. Omitted or empty values resolve to `'auto'`. */
dir?: string
/** When true, the content should not be translated (Do Not Translate). */
dnt?: boolean
Expand Down
7 changes: 4 additions & 3 deletions src/classes/MsgMessage/MsgMessage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { MessageFormat, type MessageFormatOptions } from "messageformat";
import { mf1ToMessage } from "@messageformat/icu-messageformat-1";
import { MsgInterface, DEFAULT_ATTRIBUTES, MSG_DEFAULT_FORMAT, type MsgAttributes, type MsgFormat, type MsgNote, type NoteTypes } from "../MsgInterface/MsgInterface.js";
import { MsgInterface, MSG_DEFAULT_FORMAT, type MsgAttributes, type MsgFormat, type MsgNote, type NoteTypes } from "../MsgInterface/MsgInterface.js";
import { applyAttributes } from "../../lib/apply-attributes.js";

/**
* Plain data used to create a {@link MsgMessage}.
Expand Down Expand Up @@ -35,8 +36,8 @@ export class MsgMessage implements MsgInterface {
this._key = key;
this._value = value;

// merge in any attributes
this._attributes = attributes ? {...DEFAULT_ATTRIBUTES, ...attributes} : {};
// Always apply library defaults so omitted or empty `dir` becomes `'auto'`.
this._attributes = applyAttributes(attributes);

// add any notes
if (notes) {
Expand Down
7 changes: 4 additions & 3 deletions src/classes/MsgResource/MsgResource.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type MsgMessageData, MsgMessage } from "../MsgMessage/MsgMessage.js";
import { DEFAULT_ATTRIBUTES, MSG_DEFAULT_FORMAT, MsgInterface, type MsgAttributes, type MsgNote, type NoteTypes } from "../MsgInterface/MsgInterface.js";
import { MSG_DEFAULT_FORMAT, MsgInterface, type MsgAttributes, type MsgNote, type NoteTypes } from "../MsgInterface/MsgInterface.js";
import { applyAttributes } from "../../lib/apply-attributes.js";
import { MsgProject } from "../MsgProject/MsgProject.js";
import { pseudoLocalize } from "../../lib/pseudo-localize.js";

Expand Down Expand Up @@ -61,7 +62,7 @@ export class MsgResource extends Map<string, MsgMessage> implements MsgInterface
this._title = title;

// Inherit the project's format unless the resource specifies its own.
this._attributes = {...DEFAULT_ATTRIBUTES, format: project.format, ...attributes};
this._attributes = applyAttributes(attributes, { format: project.format });
this._project = project;

if (notes) {
Expand Down Expand Up @@ -96,7 +97,7 @@ export class MsgResource extends Map<string, MsgMessage> implements MsgInterface

/** Replaces the resource's locale and formatting metadata. */
public set attributes(attributes: MsgAttributes) {
this._attributes = attributes;
this._attributes = applyAttributes(attributes);
}

/** Notes attached to this resource for translators and tooling. */
Expand Down
18 changes: 18 additions & 0 deletions src/lib/apply-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { DEFAULT_ATTRIBUTES, type MsgAttributes } from '../classes/MsgInterface/MsgInterface.js';

/**
* Merges {@link DEFAULT_ATTRIBUTES} with optional extras and caller attributes.
*
* An empty-string `dir` is treated as unset and becomes `'auto'`. This helper
* is module-internal (not re-exported from the public API).
*/
export function applyAttributes(
attributes?: MsgAttributes,
extras?: MsgAttributes
): MsgAttributes {
const merged: MsgAttributes = { ...DEFAULT_ATTRIBUTES, ...extras, ...attributes };
if (!merged.dir) {
merged.dir = 'auto';
}
return merged;
}
47 changes: 42 additions & 5 deletions src/tests/MsgMessage.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest';
import { MsgMessage } from '../classes/MsgMessage/MsgMessage.js';
import { MsgNote } from '../classes/MsgInterface/MsgInterface.js';
import { DEFAULT_ATTRIBUTES, MsgNote } from '../classes/MsgInterface/MsgInterface.js';


describe('MsgMessage tests', () => {
Expand All @@ -13,7 +13,8 @@ describe('MsgMessage tests', () => {

expect(msg1.key).toBe('my-key');
expect(msg1.value).toBe('My Value');
expect(msg1.attributes).toStrictEqual({});
expect(msg1.attributes).toStrictEqual(DEFAULT_ATTRIBUTES);
expect(msg1.attributes.dir).toBe('auto');
expect(msg1.notes).toStrictEqual([]);
});

Expand Down Expand Up @@ -65,7 +66,7 @@ describe('MsgMessage tests', () => {

expect(msg4.key).toBe('my-key');
expect(msg4.value).toBe('My Value');
expect(msg4.attributes).toStrictEqual({});
expect(msg4.attributes).toStrictEqual(DEFAULT_ATTRIBUTES);
expect(msg4.notes).toStrictEqual([{type: 'DESCRIPTION', content: 'This is a test description'}]);
expect(msg4.notes.length).toBe(1);

Expand Down Expand Up @@ -150,13 +151,49 @@ describe('MsgMessage tests', () => {
value: 'My Value'
});

expect(msg5.attributes).toStrictEqual({});
expect(msg5.attributes).toStrictEqual(DEFAULT_ATTRIBUTES);
});

test('Create with empty-string dir normalizes to auto', () => {
const msg = MsgMessage.create({
key: 'my-key',
value: 'My Value',
attributes: {
lang: 'en',
dir: ''
}
});

expect(msg.attributes.lang).toBe('en');
expect(msg.attributes.dir).toBe('auto');
});

test('Create with explicit dir is preserved', () => {
const ltr = MsgMessage.create({
key: 'ltr-key',
value: 'Left',
attributes: { dir: 'ltr' }
});
const rtl = MsgMessage.create({
key: 'rtl-key',
value: 'Right',
attributes: { dir: 'rtl' }
});
const auto = MsgMessage.create({
key: 'auto-key',
value: 'Auto',
attributes: { dir: 'auto' }
});

expect(ltr.attributes.dir).toBe('ltr');
expect(rtl.attributes.dir).toBe('rtl');
expect(auto.attributes.dir).toBe('auto');
});

const output = JSON.stringify({
"key": "my-key",
"value": "My Value",
"attributes": {}
"attributes": DEFAULT_ATTRIBUTES
}, null, 2);

test('Test generic functions', () => {
Expand Down
49 changes: 49 additions & 0 deletions src/tests/MsgResource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,35 @@ describe('MsgResource tests', () => {
expect(resource.attributes.dir).toBe('auto'); // default value
});

test('MsgResource: "create" normalizes empty-string dir to auto', () => {
const project = MsgProject.create(testProjectData);
const resource = MsgResource.create({
title: 'TestResource',
attributes: {
lang: 'en',
dir: ''
}
}, project);

expect(resource.attributes.lang).toBe('en');
expect(resource.attributes.dir).toBe('auto');
});

test('MsgResource: "create" preserves explicit dir', () => {
const project = MsgProject.create(testProjectData);
const ltr = MsgResource.create({
title: 'LtrResource',
attributes: { lang: 'en', dir: 'ltr' }
}, project);
const rtl = MsgResource.create({
title: 'RtlResource',
attributes: { lang: 'ar', dir: 'rtl' }
}, project);

expect(ltr.attributes.dir).toBe('ltr');
expect(rtl.attributes.dir).toBe('rtl');
});

test('MsgResource: title getter and setter', () => {
const project = MsgProject.create(testProjectData);
const resource = MsgResource.create({
Expand Down Expand Up @@ -197,6 +226,10 @@ describe('MsgResource tests', () => {
expect(resource.attributes.lang).toBe('fr');
expect(resource.attributes.dir).toBe('rtl');
expect(resource.attributes.dnt).toBe(true);

resource.attributes = { lang: 'de', dir: '' };
expect(resource.attributes.lang).toBe('de');
expect(resource.attributes.dir).toBe('auto');
});

test('MsgResource: notes getter and setter', () => {
Expand Down Expand Up @@ -279,6 +312,22 @@ describe('MsgResource tests', () => {
expect(message?.attributes.dnt).toBe(true); // overridden
});

test('MsgResource: "add" method normalizes empty-string dir override to auto', () => {
const project = MsgProject.create(testProjectData);
const resource = MsgResource.create({
title: 'TestResource',
attributes: {
lang: 'en',
dir: 'ltr'
}
}, project);

resource.add('test-key', 'Test Value', { dir: '' });

expect(resource.get('test-key')?.attributes.dir).toBe('auto');
expect(resource.get('test-key')?.attributes.lang).toBe('en');
});

test('MsgResource: "add" method with notes', () => {
const project = MsgProject.create(testProjectData);
const resource = MsgResource.create({
Expand Down
Loading