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
25 changes: 24 additions & 1 deletion src/classes/MsgInterface/MsgInterface.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
type NoteTypes = 'DESCRIPTION' | 'AUTHORSHIP' | 'PARAMETERS' | 'CONTEXT' | 'COMMENT';
/**
* Categories of notes that can be attached to a message or resource.
*/
export type NoteTypes = 'DESCRIPTION' | 'AUTHORSHIP' | 'PARAMETERS' | 'CONTEXT' | 'COMMENT';

/**
* A free-form note attached to a message or resource for translators and tools.
*/
export type MsgNote = {
/** What kind of note this is. */
type: NoteTypes
/** The note text. */
content: string;
}

Expand All @@ -20,20 +28,35 @@ export type MsgFormat = 'MF1' | 'MF2' | 'NONE';
*/
export const MSG_DEFAULT_FORMAT: MsgFormat = 'MF2';

/**
* Locale and formatting metadata shared by messages and resources.
*/
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`. */
dir?: string
/** When true, the content should not be translated (Do Not Translate). */
dnt?: boolean
/** MessageFormat syntax used by the string, if any. */
format?: MsgFormat
}

/**
* Default attribute values applied when none are provided.
*/
export const DEFAULT_ATTRIBUTES: MsgAttributes = {
lang: 'und',
dir: 'auto',
dnt: false
}

/**
* Shared shape for objects that carry attributes and translator notes.
*/
export interface MsgInterface {
/** Locale and formatting metadata. */
attributes: MsgAttributes
/** Notes for translators and tooling. */
notes: MsgNote[]
}
51 changes: 51 additions & 0 deletions src/classes/MsgMessage/MsgMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,35 @@ 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 } from "../MsgInterface/MsgInterface.js";

/**
* Plain data used to create a {@link MsgMessage}.
*/
export type MsgMessageData = {
/** Unique key that identifies this message within a resource. */
key: string
/** The message string, possibly in MessageFormat syntax. */
value: string
/** Optional locale and formatting metadata. */
attributes?: MsgAttributes;
/** Optional notes for translators and tooling. */
notes?: MsgNote[]
}

/**
* A single localizable message with a key, value, attributes, and notes.
*
* Supports MessageFormat 1, MessageFormat 2, and unformatted (`NONE`) strings.
*/
export class MsgMessage implements MsgInterface {
private _key: string;
private _value: string;
private _mf?: MessageFormat;
private _attributes: MsgAttributes;
private _notes: MsgNote[] = [];

/**
* Creates a message from its parts. Prefer {@link MsgMessage.create}.
*/
private constructor(key: string, value: string, attributes?: MsgAttributes, notes?: MsgNote[]) {
this._key = key;
this._value = value;
Expand All @@ -30,28 +45,38 @@ export class MsgMessage implements MsgInterface {

}

/**
* Builds a message from {@link MsgMessageData}.
*/
static create(data: MsgMessageData) {
const { key, value, attributes, notes } = data;
const message = new MsgMessage(key, value, attributes, notes);
return message;
}

/** Unique key that identifies this message within a resource. */
public get key() {
return this._key;
}

/** The message string, possibly in MessageFormat syntax. */
public get value() {
return this._value;
}

/** Locale and formatting metadata for this message. */
public get attributes() {
return this._attributes;
}

/** Notes attached to this message for translators and tooling. */
public get notes() {
return this._notes;
}

/**
* Appends a note to this message.
*/
public addNote(note: MsgNote) {
this.notes.push(note);
}
Expand All @@ -63,6 +88,9 @@ export class MsgMessage implements MsgInterface {
return this.attributes.format ?? MSG_DEFAULT_FORMAT;
}

/**
* Returns a cached MessageFormat formatter for this message's value.
*/
private getFormatter(options?: MessageFormatOptions): MessageFormat {
if (!this._mf) {
this._mf = this.resolveFormat() === 'MF1'
Expand All @@ -72,6 +100,11 @@ export class MsgMessage implements MsgInterface {
return this._mf;
}

/**
* Formats the message with the given placeholder data.
*
* For `NONE` format messages, returns the raw value unchanged.
*/
public format(data: Record<string, any>, options?: MessageFormatOptions) {
// NONE messages are not formatted; the raw string is returned as-is.
if (this.resolveFormat() === 'NONE') {
Expand All @@ -80,6 +113,11 @@ export class MsgMessage implements MsgInterface {
return this.getFormatter(options).format(data);
}

/**
* Formats the message into structured parts with the given placeholder data.
*
* For `NONE` format messages, returns a single text part with the raw value.
*/
public formatToParts(data: Record<string, any>, options?: MessageFormatOptions) {
// NONE messages have no placeholders to resolve; return the raw text part.
if (this.resolveFormat() === 'NONE') {
Expand All @@ -88,6 +126,11 @@ export class MsgMessage implements MsgInterface {
return this.getFormatter(options).formatToParts(data);
}

/**
* Returns a plain data object for this message.
*
* @param stripNotes - When true, notes are omitted from the result.
*/
public getData(stripNotes: boolean = false) {
return {
key: this.key,
Expand All @@ -97,10 +140,18 @@ export class MsgMessage implements MsgInterface {
}
}

/**
* Returns the message value as a string.
*/
public toString() {
return this.value;
}

/**
* Serializes this message to a formatted JSON string.
*
* @param stripNotes - When true, notes are omitted from the serialized data.
*/
public toJSON(stripNotes: boolean = false) {
return JSON.stringify(this.getData(stripNotes), null, 2);
}
Expand Down
57 changes: 54 additions & 3 deletions src/classes/MsgProject/MsgProject.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,66 @@
import { type MsgResourceData } from "../MsgResource/MsgResource.js";
import { type MsgFormat, MSG_DEFAULT_FORMAT } from "../MsgInterface/MsgInterface.js";

type MsgProjectSettings = {
/**
* Project-level settings such as name, version, and default message format.
*/
export type MsgProjectSettings = {
/** Display name of the localization project. */
name: string
/** Optional project version number. */
version?: number
/** Default message format inherited by resources and messages. */
format?: MsgFormat
};

type MsgTargetLocalesSettings = {
/**
* Map of a requested locale to the chain of locales used to load translations.
*/
export type MsgTargetLocalesSettings = {
[key: string]: string[]
}

type MsgLocalesSettings = {
/**
* Locale configuration for a project: source, pseudo, and target locales.
*/
export type MsgLocalesSettings = {
/** Locale of the source (authoring) strings. */
sourceLocale: string
/** Locale used for pseudo-localization during development. */
pseudoLocale: string
/** Target locales and their fallback load chains. */
targetLocales: MsgTargetLocalesSettings
};

/**
* Async function that loads translated resource data for a project, title, and language.
*/
export type MsgTranslationLoader = (project: string, title: string, lang: string) => Promise<MsgResourceData>;

/**
* Plain data used to create a {@link MsgProject}.
*/
export type MsgProjectData = {
/** Project name, version, and default format. */
project: MsgProjectSettings
/** Source, pseudo, and target locale settings. */
locales: MsgLocalesSettings
/** Function used to load translations for a locale. */
loader: MsgTranslationLoader
};

/**
* Default project settings applied when none are provided.
*/
const defaultProjectSettings: MsgProjectSettings = {
name: 'messages',
version: 1,
format: MSG_DEFAULT_FORMAT
};

/**
* Default locale settings applied when none are provided.
*/
const defaultLocalesSettings: MsgLocalesSettings = {
sourceLocale: 'en',
pseudoLocale: 'en-XA',
Expand All @@ -39,16 +69,31 @@ const defaultLocalesSettings: MsgLocalesSettings = {
}
};

/**
* A localization project that holds settings, locales, and a translation loader.
*
* Resources are created against a project so they can inherit format defaults
* and load translations through the project's loader.
*/
export class MsgProject {
/** Project name, version, and default format. */
_project: MsgProjectSettings;
/** Source, pseudo, and target locale settings. */
_locales: MsgLocalesSettings;
/** Function used to load translations for a locale. */
_loader: MsgTranslationLoader;

/**
* Builds a project from {@link MsgProjectData}.
*/
static create(data: MsgProjectData) {
const { project, locales, loader } = data;
return new MsgProject(project, locales, loader);
}

/**
* Creates a project from its settings and loader. Prefer {@link MsgProject.create}.
*/
private constructor(
projectSettings: MsgProjectSettings,
localesSettings: MsgLocalesSettings,
Expand All @@ -59,14 +104,17 @@ export class MsgProject {
this._loader = loader;
}

/** Project name, version, and default format. */
public get project() {
return this._project;
}

/** Source, pseudo, and target locale settings. */
public get locales() {
return this._locales;
}

/** Function used to load translations for a locale. */
public get loader() {
return this._loader;
}
Expand All @@ -79,6 +127,9 @@ export class MsgProject {
return this._project.format ?? MSG_DEFAULT_FORMAT;
}

/**
* Returns the translation load chain for a locale, or `undefined` if unsupported.
*/
public getTargetLocale(locale: string): string[] | undefined {
return this._locales.targetLocales[locale];
}
Expand Down
Loading
Loading