-
Notifications
You must be signed in to change notification settings - Fork 24.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(localize): support Application Resource Bundle (ARB) translation…
… file format (#36795) The ARB format is a JSON file containing an object where the keys are the message ids and the values are the translations. It is extensible because it can also contain metadata about each message. For example: ``` { "@@Locale": "...", "message-id": "Translated message string", "@message-id": { "type": "text", "description": "Some description text", "x-locations": [{ "start": {"line": 23, "column": 145}, "file": "some/file.ts" }] }, } ``` For more information, see: https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification PR Close #36795
- Loading branch information
1 parent
94e790d
commit 5684ac5
Showing
8 changed files
with
458 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
packages/localize/src/tools/src/extract/translation_files/arb_translation_serializer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
import {AbsoluteFsPath, FileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; | ||
import {ɵParsedMessage, ɵSourceLocation} from '@angular/localize'; | ||
import {ArbJsonObject, ArbLocation, ArbMetadata} from '../../translate/translation_files/translation_parsers/arb_translation_parser'; | ||
import {TranslationSerializer} from './translation_serializer'; | ||
import {consolidateMessages, hasLocation} from './utils'; | ||
|
||
/** | ||
* A translation serializer that can render JSON formatted as an Application Resource Bundle (ARB). | ||
* | ||
* See https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification | ||
* | ||
* ``` | ||
* { | ||
* "@@locale": "en-US", | ||
* "message-id": "Target message string", | ||
* "@message-id": { | ||
* "type": "text", | ||
* "description": "Some description text", | ||
* "x-locations": [ | ||
* { | ||
* "start": {"line": 23, "column": 145}, | ||
* "end": {"line": 24, "column": 53}, | ||
* "file": "some/file.ts" | ||
* }, | ||
* ... | ||
* ] | ||
* }, | ||
* ... | ||
* } | ||
* ``` | ||
*/ | ||
|
||
/** | ||
* This is a semi-public bespoke serialization format that is used for testing and sometimes as a | ||
* format for storing translations that will be inlined at runtime. | ||
* | ||
* @see ArbTranslationParser | ||
*/ | ||
export class ArbTranslationSerializer implements TranslationSerializer { | ||
constructor( | ||
private sourceLocale: string, private basePath: AbsoluteFsPath, private fs: FileSystem) {} | ||
|
||
serialize(messages: ɵParsedMessage[]): string { | ||
const messageMap = consolidateMessages(messages, message => message.customId || message.id); | ||
|
||
let output = `{\n "@@locale": ${JSON.stringify(this.sourceLocale)}`; | ||
|
||
for (const [id, duplicateMessages] of messageMap.entries()) { | ||
const message = duplicateMessages[0]; | ||
output += this.serializeMessage(id, message); | ||
output += this.serializeMeta( | ||
id, message.description, duplicateMessages.filter(hasLocation).map(m => m.location)); | ||
} | ||
|
||
output += '\n}'; | ||
|
||
return output; | ||
} | ||
|
||
private serializeMessage(id: string, message: ɵParsedMessage): string { | ||
return `,\n ${JSON.stringify(id)}: ${JSON.stringify(message.text)}`; | ||
} | ||
|
||
private serializeMeta(id: string, description: string|undefined, locations: ɵSourceLocation[]): | ||
string { | ||
const meta: string[] = []; | ||
|
||
if (description) { | ||
meta.push(`\n "description": ${JSON.stringify(description)}`); | ||
} | ||
|
||
if (locations.length > 0) { | ||
let locationStr = `\n "x-locations": [`; | ||
for (let i = 0; i < locations.length; i++) { | ||
locationStr += (i > 0 ? ',\n' : '\n') + this.serializeLocation(locations[i]); | ||
} | ||
locationStr += '\n ]'; | ||
meta.push(locationStr); | ||
} | ||
|
||
return meta.length > 0 ? `,\n ${JSON.stringify('@' + id)}: {${meta.join(',')}\n }` : ''; | ||
} | ||
|
||
private serializeLocation({file, start, end}: ɵSourceLocation): string { | ||
return [ | ||
` {`, | ||
` "file": ${JSON.stringify(this.fs.relative(this.basePath, file))},`, | ||
` "start": { "line": "${start.line}", "column": "${start.column}" },`, | ||
` "end": { "line": "${end.line}", "column": "${end.column}" }`, | ||
` }`, | ||
].join('\n'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
...e/src/tools/src/translate/translation_files/translation_parsers/arb_translation_parser.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
import {ɵMessageId, ɵparseTranslation, ɵSourceLocation, ɵSourceMessage} from '@angular/localize'; | ||
import {Diagnostics} from '../../../diagnostics'; | ||
import {ParseAnalysis, ParsedTranslationBundle, TranslationParser} from './translation_parser'; | ||
|
||
export interface ArbJsonObject extends Record<ɵMessageId, ɵSourceMessage|ArbMetadata> { | ||
'@@locale': string; | ||
} | ||
|
||
export interface ArbMetadata { | ||
type?: 'text'|'image'|'css'; | ||
description?: string; | ||
['x-locations']?: ArbLocation[]; | ||
} | ||
|
||
export interface ArbLocation { | ||
start: {line: number, column: number}; | ||
end: {line: number, column: number}; | ||
file: string; | ||
} | ||
|
||
/** | ||
* A translation parser that can parse JSON formatted as an Application Resource Bundle (ARB). | ||
* | ||
* See https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification | ||
* | ||
* ``` | ||
* { | ||
* "@@locale": "en-US", | ||
* "message-id": "Target message string", | ||
* "@message-id": { | ||
* "type": "text", | ||
* "description": "Some description text", | ||
* "x-locations": [ | ||
* { | ||
* "start": {"line": 23, "column": 145}, | ||
* "end": {"line": 24, "column": 53}, | ||
* "file": "some/file.ts" | ||
* }, | ||
* ... | ||
* ] | ||
* }, | ||
* ... | ||
* } | ||
* ``` | ||
*/ | ||
export class ArbTranslationParser implements TranslationParser<ArbJsonObject> { | ||
/** | ||
* @deprecated | ||
*/ | ||
canParse(filePath: string, contents: string): ArbJsonObject|false { | ||
const result = this.analyze(filePath, contents); | ||
return result.canParse && result.hint; | ||
} | ||
|
||
analyze(_filePath: string, contents: string): ParseAnalysis<ArbJsonObject> { | ||
const diagnostics = new Diagnostics(); | ||
if (!contents.includes('"@@locale"')) { | ||
return {canParse: false, diagnostics}; | ||
} | ||
try { | ||
// We can parse this file if it is valid JSON and contains the `"@@locale"` property. | ||
return {canParse: true, diagnostics, hint: this.tryParseArbFormat(contents)}; | ||
} catch { | ||
diagnostics.warn('File is not valid JSON.'); | ||
return {canParse: false, diagnostics}; | ||
} | ||
} | ||
|
||
parse(_filePath: string, contents: string, arb: ArbJsonObject = this.tryParseArbFormat(contents)): | ||
ParsedTranslationBundle { | ||
const bundle: ParsedTranslationBundle = { | ||
locale: arb['@@locale'], | ||
translations: {}, | ||
diagnostics: new Diagnostics() | ||
}; | ||
|
||
for (const messageId of Object.keys(arb)) { | ||
if (messageId.startsWith('@')) { | ||
// Skip metadata keys | ||
continue; | ||
} | ||
const targetMessage = arb[messageId] as string; | ||
bundle.translations[messageId] = ɵparseTranslation(targetMessage); | ||
} | ||
return bundle; | ||
} | ||
|
||
private tryParseArbFormat(contents: string): ArbJsonObject { | ||
const json = JSON.parse(contents); | ||
if (typeof json['@@locale'] !== 'string') { | ||
throw new Error('Missing @@locale property.'); | ||
} | ||
return json; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.