Skip to content

Commit

Permalink
Merge pull request #52 from rvanbekkum/feature/50-xlf2-states
Browse files Browse the repository at this point in the history
Improved XLIFF 2.0 Support
  • Loading branch information
rvanbekkum authored Aug 8, 2020
2 parents 360e26d + a44947c commit 5444d08
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 25 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## [0.5.0] 08-08-2020

* Better XLIFF 2.0 support:
* `state` attribute on `segment` nodes instead of target nodes.
* `state` and `subState` used:
* `needs-translation` -> `initial` with no sub-state
* `needs-adaptation` -> `translated` with sub-state configurable with setting `xliffSync.needsWorkTranslationSubstate`.
* `translated` -> `translated` with no sub-state.
* Let `xliffSync.fileType` = `xlf2` work with file-extension `xlf`.
* Fix: function `findXliffSyncNoteIndex` should check for the "category" attribute instead of the "from" attribute.
* Fix: function `tryDeleteXliffSyncNote` should call `findXliffSyncNoteIndex` with `notesParent` as argument instead of `unit`.
* Fix: function `setXliffSyncNote` should only add a new `notes` node in XLIFF 2.0 files if it does not exist for a unit.
* Fix: function `setXliffSyncNote` should call `findXliffSyncNoteIndex` to check if an XLIFF Sync note already exists.
* "Check for Need Work Translations" now considers the `xliffSync.needsWorkTranslationSubstate` sub-state.
* Decoration is now also applied on `segment` nodes with the `xliffSync.needsWorkTranslationSubstate` sub-state.

### Thank You

* **[antpyykk](https://github.com/antpyykk)** for filing the issue with states for XLIFF 2.0 files (GitHub issue [#50](https://github.com/rvanbekkum/vsc-xliff-sync/issues/50))

## [0.4.0] 02-08-2020

* New setting `xliffSync.syncCrossWorkspaceFolders` which can be used to set that the extension should synchronize from one single base file (`xliffSync.baseFile`) to the translation files in all workspace folders (**Default**: `false`) (GitHub issue [#48](https://github.com/rvanbekkum/vsc-xliff-sync/issues/48)).
Expand All @@ -18,6 +38,10 @@
* You can configure for which programming languages the snippets should be available with setting `xliffSync.enableSnippetsForLanguages`. Currently only the "AL Language" is supported with snippets: `tcaptionwithtranslation`, `tcommentwithtranslation`, `toptioncaptionwithtranslation`, `tpromotedactioncategorieswithtranslation`, `tlabelwithtranslation` and `ttooltipwithtranslation` snippets.
* You can configure a default target language that should be used by the snippets with setting `xliffSync.snippetTargetLanguage`.

### Thank You

* **[GregoryAA](https://github.com/GregoryAA)** for requesting the _Parse from Developer Note_ feature enhancements. (GitHub issue [#43](https://github.com/rvanbekkum/vsc-xliff-sync/issues/43))

## [0.3.7] 18-05-2020

* New setting `xliffSync.detectSourceTextChanges` (see README)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Apart from synchronizing trans-units from a base-XLIFF file, this extension cont
| xliffSync.fileType | `xlf` | The file type (`xlf` or `xlf2`). |
| xliffSync.syncCrossWorkspaceFolders | `false` | Specifies whether the extension will sync from a base file to the translation files in all workspace folders. By default, the extension will always sync. per workspace folder. If you enable this setting, then you can have the base file in one workspace folder and target translation files in other workspace folders. |
| xliffSync.missingTranslation | `%EMPTY%` | The placeholder for missing translations for trans-units that were synced/merged into target XLIFF files. You can use `%EMPTY%` if you want to use an empty string for missing translations. |
| xliffSync.needsWorkTranslationSubstate | `xliffSync:needsWork` | Specifies the substate to use for translations that need work in xlf2 files. **Tip**: If you use [Poedit](https://poedit.net/), then you could also set this to `poedit:fuzzy`. |
| xliffSync.findByXliffGeneratorNoteAndSource | `true` | Specifies whether or not the extension will try to find trans-units by XLIFF generator note and source. |
| xliffSync.findByXliffGeneratorAndDeveloperNote | `true` | Specifies whether or not the extension will try to find trans-units by XLIFF generator note and developer note. |
| xliffSync.findByXliffGeneratorNote | `true` | Specifies whether or not the extension will try to find trans-units by XLIFF generator note. |
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "xliff-sync",
"displayName": "XLIFF Sync",
"description": "A tool to keep XLIFF translation files in sync.",
"version": "0.4.0",
"version": "0.5.0",
"publisher": "rvanbekkum",
"repository": {
"type": "git",
Expand Down Expand Up @@ -151,6 +151,12 @@
"description": "Target tag content for missing translation (use %EMPTY% to leave new targets empty).",
"scope": "resource"
},
"xliffSync.needsWorkTranslationSubstate": {
"type": "string",
"default": "xliffSync:needsWork",
"description": "Specifies the substate to use for translations that need work in xlf2 files.",
"scope": "resource"
},
"xliffSync.developerNoteDesignation": {
"type": "string",
"default": "Developer",
Expand Down
20 changes: 16 additions & 4 deletions src/features/tools/files-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,24 +130,26 @@ export class FilesHelper {
return sourceUri;
}

public static async findTranslationFiles(fileExt: string, workspaceFolder?: WorkspaceFolder): Promise<Uri[]> {
public static async findTranslationFiles(fileType: string, workspaceFolder?: WorkspaceFolder): Promise<Uri[]> {
if (workspaceFolder) {
return await FilesHelper.findTranslationFilesInWorkspaceFolder(fileExt, workspaceFolder);
return await FilesHelper.findTranslationFilesInWorkspaceFolder(fileType, workspaceFolder);
}
else {
let allFileUris: Uri[] = [];
const workspaceFolders: WorkspaceFolder[] | undefined = await WorkspaceHelper.getWorkspaceFolders(true);
if (workspaceFolders) {
for (let wsFolder of workspaceFolders) {
let folderFileUris: Uri[] = await FilesHelper.findTranslationFilesInWorkspaceFolder(fileExt, wsFolder);
let folderFileUris: Uri[] = await FilesHelper.findTranslationFilesInWorkspaceFolder(fileType, wsFolder);
allFileUris = allFileUris.concat(folderFileUris);
}
}
return allFileUris;
}
}

public static async findTranslationFilesInWorkspaceFolder(fileExt: string, workspaceFolder: WorkspaceFolder): Promise<Uri[]> {
public static async findTranslationFilesInWorkspaceFolder(fileType: string, workspaceFolder: WorkspaceFolder): Promise<Uri[]> {
const fileExt: string = FilesHelper.getTranslationFileExtensions(fileType);

let relativePattern: RelativePattern = new RelativePattern(workspaceFolder, `**/*.${fileExt}`);
return workspace.findFiles(relativePattern).then((files) =>
files.sort((a, b) => {
Expand All @@ -159,6 +161,16 @@ export class FilesHelper {
);
}

public static getTranslationFileExtensions(fileType: string): string {
switch(fileType) {
case 'xlf':
return 'xlf';
case 'xlf2':
return 'xlf*';
}
return 'xlf';
}

public static async createTranslationFile(
language: string,
baseUri: Uri,
Expand Down
3 changes: 2 additions & 1 deletion src/features/tools/xlf-translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import { workspace, WorkspaceConfiguration, WorkspaceFolder } from 'vscode';
import { XlfDocument } from './xlf/xlf-document';
import { translationState } from './xlf/xlf-translationState';

export class XlfTranslator {
public static async synchronize(
Expand Down Expand Up @@ -160,7 +161,7 @@ export class XlfTranslator {

if (mergedSourceText !== origSourceText) {
mergedDocument.setXliffSyncNote(unit, 'Source text has changed. Please review the translation.');
mergedDocument.setTargetAttribute(unit, 'state', 'needs-adaptation');
mergedDocument.setState(unit, translationState.needsWorkTranslation);
}
}
}
Expand Down
139 changes: 126 additions & 13 deletions src/features/tools/xlf/xlf-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import { XmlNode, XmlParser, XmlBuilder } from '..';
import { workspace, Uri, WorkspaceConfiguration } from 'vscode';
import { translationState } from './xlf-translationState';

export class XlfDocument {
public get valid(): boolean {
Expand Down Expand Up @@ -157,6 +158,7 @@ export class XlfDocument {
private preserveTargetOrder: boolean;
private parseFromDeveloperNoteSeparator: string;
private missingTranslation: string;
private needsWorkTranslationSubstate: string;

private constructor(resourceUri: Uri | undefined) {
const xliffWorkspaceConfiguration: WorkspaceConfiguration = workspace.getConfiguration('xliffSync', resourceUri);
Expand All @@ -170,6 +172,7 @@ export class XlfDocument {
if (this.missingTranslation === '%EMPTY%') {
this.missingTranslation = '';
}
this.needsWorkTranslationSubstate = xliffWorkspaceConfiguration['needsWorkTranslationSubstate'];
}

public static async load(source: string, resourceUri: Uri | undefined): Promise<XlfDocument> {
Expand Down Expand Up @@ -449,10 +452,10 @@ export class XlfDocument {
let attributes: { [key: string]: string; } = {};
if (!translation) {
translation = this.missingTranslation;
attributes['state'] = 'needs-translation';
this.updateStateAttributes(attributes, translationState.missingTranslation);
}
else {
attributes['state'] = 'translated';
this.updateStateAttributes(attributes, translationState.translated);
}

targetNode = this.createTargetNode(sourceUnit, attributes, translation);
Expand All @@ -467,12 +470,46 @@ export class XlfDocument {
if (!targetNode.attributes) {
targetNode.attributes = {};
}
targetNode.attributes['state'] = 'translated';
this.updateStateAttributes(targetNode.attributes, translationState.translated);
}
this.appendTargetNode(sourceUnit, targetNode);
}
}

public updateStateAttributes(attributes: { [key: string]: string; }, newState: translationState) {
switch (this.version) {
case '1.2':
switch (newState) {
case translationState.missingTranslation:
attributes['state'] = 'needs-translation';
break;
case translationState.needsWorkTranslation:
attributes['state'] = 'needs-adaptation';
break;
case translationState.translated:
attributes['state'] = 'translated';
break;
}
break;
case '2.0':
switch (newState) {
case translationState.missingTranslation:
attributes['state'] = 'initial';
delete attributes["subState"];
break;
case translationState.needsWorkTranslation:
attributes['state'] = 'translated';
attributes['subState'] = this.needsWorkTranslationSubstate;
break;
case translationState.translated:
attributes['state'] = 'translated';
delete attributes["subState"];
break;
}
break;
}
}

public createTargetNode(parentUnit: XmlNode, attributes: { [key: string]: string; }, translation: string): XmlNode {
return {
name: 'target',
Expand Down Expand Up @@ -548,8 +585,60 @@ export class XlfDocument {
return undefined;
}

public getState(unit: XmlNode): translationState | undefined {
let stateNode: XmlNode | undefined = this.tryGetStateNode(unit);
if (stateNode && stateNode.attributes) {
switch (this.version) {
case '1.2':
{
const state: string | undefined = stateNode.attributes['state'];
switch (state) {
case 'needs-translation':
return translationState.missingTranslation;
case 'needs-adaptation':
return translationState.needsWorkTranslation;
case 'translated':
return translationState.translated;
}
break;
}
case '2.0':
{
const state: string | undefined = stateNode.attributes['state'];
switch (state) {
case 'initial':
return translationState.missingTranslation;
case 'translated':
if (stateNode.attributes['subState'] === this.needsWorkTranslationSubstate) {
return translationState.needsWorkTranslation;
}
else {
return translationState.translated;
}
}
}
}
}
return undefined;
}

private tryGetStateNode(unit: XmlNode): XmlNode | undefined {
let stateNodeTag: string = 'target';
switch (this.version) {
case '1.2':
stateNodeTag = 'target';
break;
case '2.0':
stateNodeTag = 'segment';
break;
}

return this.getNode(stateNodeTag, unit);
}


public setTargetAttribute(unit: XmlNode, attribute: string, attributeValue: string) {
let targetNode = this.getNode('target', unit);
let targetNode: XmlNode | undefined = this.getNode('target', unit);
if (!targetNode) {
let attributes: { [key: string]: string; } = {};
attributes[attribute] = attributeValue;
Expand All @@ -561,6 +650,19 @@ export class XlfDocument {
}
}

public setState(unit: XmlNode, newState: translationState) {
let stateNode = this.tryGetStateNode(unit);
if (!stateNode && this.version === '1.2') {
let attributes: { [key: string]: string; } = {};
this.updateStateAttributes(attributes, newState);
let targetNode: XmlNode = this.createTargetNode(unit, attributes, "");
this.appendTargetNode(unit, targetNode);
}
else if (stateNode) {
this.updateStateAttributes(stateNode.attributes, newState);
}
}

private deleteTargetNode(unit: XmlNode) {
if (unit) {
const index = unit.children.indexOf('target', 0);
Expand All @@ -583,17 +685,17 @@ export class XlfDocument {
notesParent = this.getNode('notes', unit);
if (!notesParent) {
notesParent = {
name: 'note',
local: 'note',
name: 'notes',
local: 'notes',
parent: unit,
attributes: {},
children: [],
isSelfClosing: false,
prefix: '',
uri: '',
};
unit.children.push(notesParent);
}
unit.children.push(notesParent);
break;
default:
return;
Expand All @@ -612,9 +714,7 @@ export class XlfDocument {
uri: '',
};

let noteIdx = notesParent.children.findIndex(
(child) => typeof child !== 'string' && child.name === 'note' && child.attributes && child.attributes['from'] === fromAttribute,
);
const noteIdx: number = this.findXliffSyncNoteIndex(notesParent);
let targetIdx = unit.children.findIndex(
(child) => typeof child !== 'string' && child && child.name === 'target',
);
Expand All @@ -632,7 +732,7 @@ export class XlfDocument {
public tryDeleteXliffSyncNote(unit: XmlNode): boolean {
let notesParent: XmlNode | undefined = this.getNotesParent(unit);
if (notesParent) {
const noteIdx: number = this.findXliffSyncNoteIndex(unit);
const noteIdx: number = this.findXliffSyncNoteIndex(notesParent);
if (noteIdx >= 0) {
let deleteCount: number = 1;

Expand All @@ -654,9 +754,22 @@ export class XlfDocument {
return -1;
}

const fromAttribute = 'XLIFF Sync';
let categoryAttributeName: string;
switch (this.version) {
case '1.2':
categoryAttributeName = 'from';
break;
case '2.0':
categoryAttributeName = 'category';
break;
default:
categoryAttributeName = 'from';
break;
}
const categoryAttributeValue: string = 'XLIFF Sync';

return notesParent.children.findIndex(
(child) => typeof child !== 'string' && child.name === 'note' && child.attributes && child.attributes['from'] === fromAttribute,
(child) => typeof child !== 'string' && child.name === 'note' && child.attributes && (child.attributes[categoryAttributeName] === categoryAttributeValue),
);
}

Expand Down
5 changes: 5 additions & 0 deletions src/features/tools/xlf/xlf-translationState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum translationState {
missingTranslation = 0,
needsWorkTranslation = 1,
translated = 10
}
Loading

0 comments on commit 5444d08

Please sign in to comment.