Skip to content

Commit

Permalink
ilib-loctool-react: Implement plugin using Node.js API
Browse files Browse the repository at this point in the history
  • Loading branch information
Natalia Kędziora committed Feb 10, 2025
1 parent 1a5176b commit d5f5390
Show file tree
Hide file tree
Showing 24 changed files with 866 additions and 77 deletions.
16 changes: 15 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,22 @@ pnpm [script-name]

TBD

To run the build, use:
#### 1. Run build for all packages

To run build for all packages in the monorepo, from the root directory, run:

```bash
pnpm build
```

#### 2. Run build for a single package in the monorepo:

1. From the root directory:
```bash
pnpm --filter loctool build
```

2. From the package directory:
```bash
pnpm build
```
Expand Down
1 change: 1 addition & 0 deletions packages/ilib-loctool-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"doc:html": "mkdir -p docs && typedoc --out docs --skipErrorChecking --cleanOutputDir false src/index.ts"
},
"devDependencies": {
"@formatjs/cli-lib": "6.6.6",
"@tsconfig/node12": "^12.1.4",
"@types/jest": "^29.5.14",
"@types/node": "12",
Expand Down
91 changes: 91 additions & 0 deletions packages/ilib-loctool-react/src/ReactFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright © 2025 JEDLSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {API, File, Project, ResourceString, TranslationSet} from "loctool";
import fs from "fs";
import {extract, mapToResources} from "./resourceUtils";

export class ReactFile implements File {
private readonly translationSet: TranslationSet<ResourceString>;

/**
* private readonly path: A reference to the path of the file.
* private readonly loctool API: A reference to the loctool {@link API} instance which provides functionality to the plugin.
*/
constructor(
private readonly path: string,
private readonly loctoolApi: API,
private readonly sourceLocale: string,
private readonly project: Project
) {
this.translationSet = this.loctoolApi.newTranslationSet(sourceLocale);
}

/**
* Extracts all the localizable strings from the file and adds them to the project's translation set.
* This method
* * opens the file,
* * reads its contents,
* * parses it and
* * adds each string it finds to the project's translation set, which can be retrieved from the API passed to the constructor.
*/
extract(): void {
extract(this.path).then((messages) => {
const resources = mapToResources({
messages,
options: {
sourceLocale: this.sourceLocale,
projectId: this.project.getProjectId(),
},
createResource: this.loctoolApi.newResource.bind(this),
});

this.translationSet.addAll(resources);
});
}

/**
* Returns the translations set (a set of ResourceString objects) for the ReactFile.
*/
getTranslationSet() {
return this.translationSet;
}

/**
* NOOP, as the source file is not modified in any way during the extraction.
*/
write() {
}

/**
* NOOP.
* This should return the location on disk where the localized version of this file should be written,
* but it looks like this method was never used by `ilib-loctool`.
*/
getLocalizedPath(_locale: string): string {
return "";
}

/**
* NOOP. `ilib-loctool-react` plugin does not write any localized content to a file.
*/
localize(
_translations: TranslationSet<ResourceString>,
_locales: string[]
) {
}
}
159 changes: 159 additions & 0 deletions packages/ilib-loctool-react/src/ReactFileType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright © 2025 JEDLSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/


import { API, FileType, Project, TranslationSet } from "loctool";
import { ReactFile } from "./ReactFile";
import path from "path";

/**
* Represents a file type that can be handled by the plugin.
*/
export class ReactFileType implements FileType {
private readonly sourceLocale: string;
private static readonly datatype = "x-react";
private static readonly extensions = [".js", ".jsx", ".ts", ".tsx"];
private ReactFileInstance: ReactFile | undefined;

/**
* A unique name for this type of plugin that can be displayed to a user.
*/
private static readonly pluginName =
"Loctool plugin for extracting localizable strings from React projects.";

/**
* private readonly project: A reference to the loctool {@link Project} instance which uses ReactFileType.
*
* private readonly loctool API: A reference to the loctool {@link API} instance which provides functionality to the `ilib-loctool-react` plugin.
*/
constructor(
private readonly project: Project,
private readonly loctoolApi: API
) {
this.sourceLocale = this.project.getSourceLocale();
}

/**
* Returns true if the given path is handled by the ReactFileType or false otherwise.
*/
handles(filePath: string): boolean {
return ReactFileType.extensions.includes(path.extname(filePath));
}

/**
* Returns an array of extensions ReactFileType can handle.
*/
getExtensions(): string[] {
return ReactFileType.extensions;
}

/**
* Returns a plugin name for ReactFileType.
*/
name(): string {
return ReactFileType.pluginName;
}

/**
* NOOP. Nothing is written out from `ilib-loctool-react` by design.
* It is used only for reading files and passing ResourceStrings back to `loctool`.
*/
write() {}

/**
* Returns a new instance of ReactFile class for the given file path.
*/
newFile(filePath: string): ReactFile {
if (this.ReactFileInstance) {
throw new Error("ReactFile instance is already initialized");
}

const fullPath = path.join(this.project.getRoot(), filePath);
this.ReactFileInstance = new ReactFile(
fullPath,
this.loctoolApi,
this.sourceLocale,
this.project
);

return this.ReactFileInstance;
}

/**
* Returns a unique string that can be used to identify strings that come from ReactFileType.
*/
getDataType(): string {
return ReactFileType.datatype;
}

/**
* Returns undefined to use defaults hash that maps the class of a resource specific for ReactFile to a resource type.
*/
getDataTypes(): undefined {
return undefined;
}

/**
* Returns the translation set containing all the extracted resources for all instances of ReactFileType.
* This includes all new strings and all existing strings extracted from a source file.
*/
getExtracted(): TranslationSet {
if (!this.ReactFileInstance) {
throw new Error("ReactFile instance is not initialized");
}

return this.ReactFileInstance.getTranslationSet();
}

/**
* NOOP, as it's not clear why would the ReactFileType need to add a set of translations to itself
* given that per the getExtracted method, sets should come from the ReactFile.
*/
addSet(_set: TranslationSet): void {}

/**
* Returns the translation set containing all the new resources for all instances of ReactFileType.
* Not sure how this differs from getExtracted method.
*/
getNew(): TranslationSet {
return this.getExtracted();
}

/**
* Return the translation set containing all the pseudo localized resources for all instances of ReactFileType.
*/
getPseudo(): TranslationSet {
return this.loctoolApi.newTranslationSet(this.sourceLocale);
}

getResourceTypes() {
/*
According to:
https://github.com/iLib-js/loctool/blob/285401359f923c1be11e7329b549ed11b4099637/lib/Project.js#L235-L244
Even though not specified in the documented plugin interface, loctool seems to expect
either getResourceTypes() or registerDataTypes() to be present.
getResourceTypes() is expected to return a mapping between the `datatype` identifier and
a class name registered in the ResourceFactory, while registerDataTypes()
appears to do a similar thing, but with internal access to the ResourceFactory,
based on existing implementations for built-in file types.
*/
return {
[ReactFileType.datatype]: "ResourceString",
} as const;
}
}
43 changes: 43 additions & 0 deletions packages/ilib-loctool-react/src/__test__/ReactFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {ReactFileType} from "../ReactFileType";
import {API, Project} from "loctool";

describe("ReactFileType", () => {
it("returns a list of handled extensions", () => {
const reactFileType = createReactFileType();

expect(reactFileType.getExtensions()).toEqual([".js", ".jsx", ".ts", ".tsx"]);
});

test("returns associated data type", () => {
const reactFileType = createReactFileType();

expect(reactFileType.getDataType()).toBe("x-react");
});

it.each([".js", ".jsx", ".ts", ".tsx"])("handles %s files", (extension) => {
const reactFileType = createReactFileType();

expect(reactFileType.handles(`file.${extension}`)).toBe(true);
});

it.each([".html", ".scss"])("does not handle unsupported file extensions, e.g. %s", (extension) => {
const reactFileType = createReactFileType();

expect(reactFileType.handles(`file.${extension}`)).toBe(false);
});

});

function createReactFileType() {
const project = {
getSourceLocale: jest.fn().mockReturnValue("en-US"),
getRoot: jest.fn().mockReturnValue("/root"),
} as unknown as Project;

const api = {
newTranslationSet: () => {
},
} as unknown as API;

return new ReactFileType(project, api);
}
5 changes: 0 additions & 5 deletions packages/ilib-loctool-react/src/__test__/index.test.ts

This file was deleted.

Loading

0 comments on commit d5f5390

Please sign in to comment.