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
3 changes: 3 additions & 0 deletions GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ Set `format` to `'MF1'`, `'MF2'`, or `'NONE'` on a project, resource, or
message. It is a TypeScript union type (there is no enum), and lower levels
inherit from higher levels unless they override it.

Message `lang`, `dir`, and `dnt` also inherit from the resource when omitted
(including in compact translation JSON). An explicit per-message value wins.

```typescript
// MF1 syntax (ICU MessageFormat 1)
resource.add('files', '{count, plural, one {# file} other {# files}}', { format: 'MF1' });
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ Every message is formatted according to its resolved `format` attribute:

The `format` is inheritable: a resource inherits its project's `format` unless it sets its own, and a message inherits its resource's `format` unless it sets its own. The default is `MF2`, so existing code keeps working unchanged. Use a TypeScript union (`'MF1' | 'MF2' | 'NONE'`) — there is no enum.

`lang`, `dir`, and `dnt` inherit the same way on messages: omitted fields (or omitted `attributes`) take the resource's values, and an explicit per-message value still wins. Compact translation JSON can therefore list only overrides. `create()`, `add()`, `translate()`, and `getTranslation()` all apply this merge.

The following is a **separate illustration** of format inheritance and per-message overrides (not the CLI `Main` / `Messages` scaffold above):

```typescript
Expand Down
23 changes: 13 additions & 10 deletions src/classes/MsgResource/MsgResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ export class MsgResource extends Map<string, MsgMessage> implements MsgInterface
/**
* Adds a message to this resource, merging resource attributes with any overrides.
*
* Unspecified `lang`, `dir`, and `dnt` inherit from this resource; explicit
* values on the message win.
*
* @returns This resource, for chaining.
*/
public add(key: string, value: string, attributes?: MsgAttributes, notes?: MsgNote[]) {
Expand All @@ -169,7 +172,8 @@ export class MsgResource extends Map<string, MsgMessage> implements MsgInterface
* Builds a translated copy of this resource from translation data.
*
* Messages present in the translation replace the source; missing keys keep
* the source message. Notes are carried over from the source.
* the source message. Notes are carried over from the source. Unspecified
* message `lang`, `dir`, and `dnt` inherit from the translated resource.
*
* @throws {TypeError} When the translation title does not match this resource.
*/
Expand All @@ -194,17 +198,16 @@ export class MsgResource extends Map<string, MsgMessage> implements MsgInterface

messages?.forEach(messageData => {
const {key, value, attributes} = messageData;
const msg = MsgMessage.create({
const source = this.get(key);
// Inherit lang/dir/dnt from the translated resource (same merge as add()),
// while still preserving the source message's format when the translation
// does not set one.
translated.add(
key,
value,
// preserve the source message's format unless the translation overrides it
attributes: this.preserveFormat(attributes, this.get(key)?.attributes.format),
});
const notes = this.get(key)?.notes || []; // transfer the notes
notes.forEach(note => {
msg.addNote(note.type, note.content);
})
translated.set(key, msg);
this.preserveFormat(attributes, source?.attributes.format),
source?.notes
);
})

return translated;
Expand Down
113 changes: 113 additions & 0 deletions src/tests/MsgResource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,37 @@ describe('MsgResource tests', () => {
expect(resource.has('test-2')).toBe(true);
expect(resource.get('test-1')?.value).toBe('This is test 1');
expect(resource.get('test-2')?.value).toBe('This is test 2');
expect(resource.get('test-1')?.attributes.lang).toBe('en');
expect(resource.get('test-1')?.attributes.dir).toBe('ltr');
expect(resource.get('test-1')?.attributes.dnt).toBe(false);
});

test('MsgResource: "create" inherits unspecified message lang, dir, and dnt from the resource', () => {
const project = MsgProject.create(testProjectData);
const resource = MsgResource.create({
title: 'TestResource',
attributes: { lang: 'en', dir: 'rtl', dnt: true },
messages: [
{ key: 'omitted', value: 'No attributes' },
{ key: 'dnt-only', value: 'DNT override', attributes: { dnt: false } },
{ key: 'lang-only', value: 'Lang override', attributes: { lang: 'fr' } }
]
}, project);

const omitted = resource.get('omitted');
expect(omitted?.attributes.lang).toBe('en');
expect(omitted?.attributes.dir).toBe('rtl');
expect(omitted?.attributes.dnt).toBe(true);

const dntOnly = resource.get('dnt-only');
expect(dntOnly?.attributes.lang).toBe('en');
expect(dntOnly?.attributes.dir).toBe('rtl');
expect(dntOnly?.attributes.dnt).toBe(false);

const langOnly = resource.get('lang-only');
expect(langOnly?.attributes.lang).toBe('fr');
expect(langOnly?.attributes.dir).toBe('rtl');
expect(langOnly?.attributes.dnt).toBe(true);
});

test('MsgResource: "create" static method with notes', () => {
Expand Down Expand Up @@ -549,6 +580,44 @@ describe('MsgResource tests', () => {
expect(msg?.attributes.dnt).toBe(true);
});

test('MsgResource: "translate" inherits unspecified message lang, dir, and dnt from the translated resource', () => {
const project = MsgProject.create(testProjectData);
const original = MsgResource.create({
title: 'TestResource',
attributes: { lang: 'en', dir: 'ltr', dnt: false },
messages: [
{ key: 'omitted', value: 'Original omitted' },
{ key: 'dnt-only', value: 'Original dnt' },
{ key: 'lang-only', value: 'Original lang' }
]
}, project);

const translated = original.translate({
title: 'TestResource',
attributes: { lang: 'zh', dir: 'rtl', dnt: true },
messages: [
{ key: 'omitted', value: '翻译 omitted' },
{ key: 'dnt-only', value: '翻译 dnt', attributes: { dnt: false } },
{ key: 'lang-only', value: '翻译 lang', attributes: { lang: 'fr' } }
]
});

const omitted = translated.get('omitted');
expect(omitted?.attributes.lang).toBe('zh');
expect(omitted?.attributes.dir).toBe('rtl');
expect(omitted?.attributes.dnt).toBe(true);

const dntOnly = translated.get('dnt-only');
expect(dntOnly?.attributes.lang).toBe('zh');
expect(dntOnly?.attributes.dir).toBe('rtl');
expect(dntOnly?.attributes.dnt).toBe(false);

const langOnly = translated.get('lang-only');
expect(langOnly?.attributes.lang).toBe('fr');
expect(langOnly?.attributes.dir).toBe('rtl');
expect(langOnly?.attributes.dnt).toBe(true);
});

test('MsgResource: "translate" method transfers notes to new messages', () => {
const project = MsgProject.create(testProjectData);
const original = MsgResource.create({
Expand Down Expand Up @@ -609,6 +678,50 @@ describe('MsgResource tests', () => {
expect(translated.size).toBe(2);
expect(translated.get('test-1')?.value).toBe('这是测试 1');
expect(translated.get('test-2')?.value).toBe('这是测试 2');
expect(translated.get('test-1')?.attributes.lang).toBe('zh');
expect(translated.get('test-1')?.attributes.dir).toBe('ltr');
expect(translated.get('test-1')?.attributes.dnt).toBe(false);
});

test('MsgResource: "getTranslation" inherits unspecified message lang, dir, and dnt from the translated resource', async () => {
const project = MsgProject.create({
...testProjectData,
loader: async () => ({
title: 'TestResource',
attributes: { lang: 'zh', dir: 'rtl', dnt: true },
messages: [
{ key: 'omitted', value: '翻译 omitted' },
{ key: 'dnt-only', value: '翻译 dnt', attributes: { dnt: false } },
{ key: 'lang-only', value: '翻译 lang', attributes: { lang: 'fr' } }
]
})
});
const resource = MsgResource.create({
title: 'TestResource',
attributes: { lang: 'en', dir: 'ltr', dnt: false },
messages: [
{ key: 'omitted', value: 'Original omitted' },
{ key: 'dnt-only', value: 'Original dnt' },
{ key: 'lang-only', value: 'Original lang' }
]
}, project);

const translated = await resource.getTranslation('zh');

const omitted = translated.get('omitted');
expect(omitted?.attributes.lang).toBe('zh');
expect(omitted?.attributes.dir).toBe('rtl');
expect(omitted?.attributes.dnt).toBe(true);

const dntOnly = translated.get('dnt-only');
expect(dntOnly?.attributes.lang).toBe('zh');
expect(dntOnly?.attributes.dir).toBe('rtl');
expect(dntOnly?.attributes.dnt).toBe(false);

const langOnly = translated.get('lang-only');
expect(langOnly?.attributes.lang).toBe('fr');
expect(langOnly?.attributes.dir).toBe('rtl');
expect(langOnly?.attributes.dnt).toBe(true);
});

test('MsgResource: "getTranslation" method without lang clones the resource', async () => {
Expand Down
Loading