owned this note
owned this note
Published
Linked with GitHub
# $localize - message-id support
## Background
The original [design doc](https://hackmd.io/UfFN_wyVT-Ob1p70nA3N_Q) for `$localize` avoids message-ids for Angular v9 since the template compiler does not need to generate messages based on ids.
Therefore the original implementation for `$localize` relies upon the source-message as the "key" for looking up translations. For example give then following template:
```htmlmixed=
<head>
<div i18n>
Hello, {{title}}!
</div>
</head>
```
The Angular compiler generates the following source-message, which is then used as a translation-key.
```typescript
" Hello, {$interpolation}! "
```
### Problems with source-message as translation-key
#### Multiple meanings
It is possible for there to be more than one translation for a source message depending upon the context, requiring a custom/manual id:
```
"right" (correct) => vrai
"right" (opposite of left) => droit
"right" (fair) => juste
```
In the curent version of Angular the message "meaning" is combined with the source-message to compute the id.
#### Whitespace removal
In order to support consistent rendering of expandable ICU messages (containing markup), the source-message has its whitespace removed, if `preserveWhitespace: false`. This means that the source-message in the template is different to that extracted into translation files. In the example given in [Background](#background) the source-message in the template is:
```typescript
" Hello, {$interpolation}! "
```
while in the translation file it is:
```typescript
"\n Hello, {$interpolation}!\n "
```
This prevents accurate translation lookup based on the source-message string.
## Proposal
### Localized string syntax
Extend the format of localized strings to allow "meaning", "description" and "message-id" to be provided. This metadata is provided at the start of the string marked by colons. Each of these is optional. For example:
```typescript
$localize`:meaning|description@@message-id:source message text`;
$localize`:meaning|:source message text`;
$localize`:description:source message text`;
$localize`:@@message-id:source message text`;
```
:::info
The delimiters within the colons are chosen to match those already used in Angular templates. For example:<br><br>
```htmlmixed
<h1
i18n="site header|An introduction header@@introductionHeader">
Hello i18n!
</h1>
```
:::
:::info
If a source-message actually starts with a colon then it must be escaped with a backslash. For example:<br><br>
```typescript
$localize`\:this message actually starts with a colon (:)`;
```
This approach is similar to that already implemented for named placeholders: the substitution is post-fixed by a colon delimited placeholder name. For example:<br><br>
```typescript
$localize`Hello ${item.name}:title:`; // placeholder name is 'title'
$localize`${hours}\:${mins}\:${secs}`; // colons are part of the message
```
:::
### Template generation
The template source-message strings are not guaranteed to be identical to those in translation files. See [Whitespace removal](#Whitespace-removal)) above. Therefore we must provide the computed id when generating `$localize` tagged strings in template generation. For example:
```typescript
$localize `:@@123456789: Hello, {$interpolation}! `;
```
This ensures that translation of this message is based on the message-id and not the source-message.
### Translation matching
Use the message-id as the key when looking up translations rather than the source-message.
If a source-message does not provide a custom message-id then compute one.
Computed message-ids are generated using the same algorithm as XLIFF2 and XMB/XTB formats. This is implemented in the `decimalDigest()` function.
### $localize.translate() implementation
The `$localize()` function passes the `messageParts` and `expressions` to the `$localize.translate()` run-time translation function. The current implementation computes the source-message and uses that as the translation-key.
```typescript
function translate(messageParts: TemplateStringsArray,
expressions: readonly any[]): [TemplateStringsArray, readonly any[]];
```
Modify this function to compute the message-id instead and use that as the translation-key.
### Translation storage
Internally each translation is stored as a `ParsedTranslation` object, which is kept in a lookup table.
```typescript
export interface ParsedTranslation {
messageParts: TemplateStringsArray;
placeholderNames: string[];
}
type SourceMessage = string;
type InternalTranslations = Record<SourceMessage, ParsedTranslation>;
```
Modify this lookup table so that the key is the message-id.
```typescript
type MessageId = string;
type InternalTranslations = Record<MessageId, ParsedTranslation>;
```
### Loading translations
The `loadTranslations()` function accepts a `translations` argument:
```typescript
export type Translations = Record<SourceMessage, TargetMessage>;
export function loadTranslations(translations: Translations): void;
```
Change this function to accept a `translations` argument that is a map of message-ids instead of source-messages:
```typescript
export type Translations = Record<MessageId, TargetMessage>;
export function loadTranslations(translations: Translations): void;
```
:::warning
When calling `loadTranslations()`, the caller is now responsible for providing the message-id of each translation: either custom message-ids or computed via `decimalDigest()`.
:::
If the translations are loaded from a formatted translation file that uses the same algorithm as `decimalDigest()`, e.g. XLIFF2 or XTB, then the loader can just use the message-ids directly.
If the translations are loaded from a formatted translation file uses a different digest algorithm, e.g. XLIFF 1.2, then the loader must convert the given message-id before calling `loadTranslations()`. This is done as follows:
* Compute the message-id, using the appropriate digest algorithm, from the source-message in the file
* Check this computed message-id against the one stored in the translation file.
* If the computed message-id is different to the one in the file, then this must be a custom message-id
* use the custom message-id unchanged
* else the message-ids are identical then this must be a computed message-id so compute a new message-id the `decimalDigest()` algorithm.
* use the new computed message-id