Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(stdlib,exts): implement Tr #578

Merged
merged 6 commits into from
Nov 17, 2020
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ types
lib
package.json
**/*.js.snap
translations.ts
25 changes: 19 additions & 6 deletions src/brsTypes/components/RoDeviceInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export class RoDeviceInfo extends BrsComponent implements BrsValue {
private enableAudioGuideChanged = BrsBoolean.True;
private enableCodecCapChanged = BrsBoolean.True;

/** A user-specified locale to use for Brightscript functions. */
private static _locale: string | undefined;

constructor() {
super("roDeviceInfo");
this.registerMethods({
Expand Down Expand Up @@ -77,6 +80,19 @@ export class RoDeviceInfo extends BrsComponent implements BrsValue {
});
}

/** Sets the locale for Brightscript functions. */
public static set locale(newLocale: string) {
RoDeviceInfo._locale = newLocale;
}

/**
* Returns the locale for Brightscript functions. If a custom
* locale has been specified, use that. Otherwise, default to the Node process.
*/
public static get locale(): string {
return RoDeviceInfo._locale || process.env.LOCALE || "";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

}

toString(parent?: BrsType): string {
return "<Component: roDeviceInfo>";
}
Expand Down Expand Up @@ -191,8 +207,7 @@ export class RoDeviceInfo extends BrsComponent implements BrsValue {
returns: ValueKind.String,
},
impl: (_interpreter) => {
let countryCode = process.env.LOCALE;
return countryCode ? new BrsString(countryCode) : new BrsString("");
return new BrsString(RoDeviceInfo.locale);
alimnios72 marked this conversation as resolved.
Show resolved Hide resolved
},
});

Expand Down Expand Up @@ -234,8 +249,7 @@ export class RoDeviceInfo extends BrsComponent implements BrsValue {
returns: ValueKind.String,
},
impl: (_interpreter) => {
let locale = process.env.LOCALE;
return locale ? new BrsString(locale) : new BrsString("");
return new BrsString(RoDeviceInfo.locale);
},
});

Expand All @@ -245,8 +259,7 @@ export class RoDeviceInfo extends BrsComponent implements BrsValue {
returns: ValueKind.String,
},
impl: (_interpreter) => {
let countryCode = process.env.LOCALE;
return countryCode ? new BrsString(countryCode) : new BrsString("");
return new BrsString(RoDeviceInfo.locale);
},
});

Expand Down
37 changes: 36 additions & 1 deletion src/extensions/Process.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,43 @@
import { RoAssociativeArray, BrsString, RoArray } from "../brsTypes";
import {
RoAssociativeArray,
BrsString,
RoArray,
Callable,
ValueKind,
RoDeviceInfo,
BrsInvalid,
StdlibArgument,
} from "../brsTypes";
import { Interpreter } from "../interpreter";

export const Process = new RoAssociativeArray([
{
name: new BrsString("argv"),
value: new RoArray(process.argv.map((arg) => new BrsString(arg))),
},
{
name: new BrsString("getLocale"),
value: new Callable("getLocale", {
signature: {
returns: ValueKind.String,
args: [],
},
impl: (_: Interpreter) => {
return new BrsString(RoDeviceInfo.locale);
},
}),
},
{
name: new BrsString("setLocale"),
value: new Callable("setLocale", {
signature: {
returns: ValueKind.Invalid,
args: [new StdlibArgument("newLocale", ValueKind.String)],
},
impl: (_: Interpreter, newLocale: BrsString) => {
RoDeviceInfo.locale = newLocale.value;
return BrsInvalid.Instance;
},
}),
},
]);
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Interpreter, ExecutionOptions, defaultExecutionOptions } from "./interp
import * as BrsError from "./Error";
import * as LexerParser from "./LexerParser";
import { CoverageCollector } from "./coverage";
import { loadTranslationFiles } from "./stdlib";

import * as _lexer from "./lexer";
export { _lexer as lexer };
Expand Down Expand Up @@ -69,6 +70,8 @@ export async function execute(filenames: string[], options: Partial<ExecutionOpt
throw new Error("Unable to build interpreter.");
}

loadTranslationFiles(interpreter, executionOptions.root);

if (executionOptions.generateCoverage) {
coverageCollector = new CoverageCollector(executionOptions.root, lexerParserFn);
await coverageCollector.crawlBrsFiles();
Expand Down
89 changes: 89 additions & 0 deletions src/stdlib/Localization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as fs from "fs";
import * as path from "path";
import { promisify } from "util";
const readFile = promisify(fs.readFile);

import { Interpreter } from "../interpreter";
import { BrsString, Callable, ValueKind, StdlibArgument, RoDeviceInfo } from "../brsTypes";
import { XmlDocument } from "xmldoc";

/**
* Supported locales in RBI.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that we need to enforce this, we could just grab whatever subdirectories exist in the locale/ directory. But it made traversal a little easier 🤷

* @see https://developer.roku.com/docs/references/brightscript/interfaces/ifdeviceinfo.md#getcurrentlocale-as-string
*/
let locales = new Set<string>([
alimnios72 marked this conversation as resolved.
Show resolved Hide resolved
"en_US", // US English
"en_GB", // British English
"fr_CA", // Canadian French
"es_ES", // International Spanish
"de_DE", // German
"it_IT", // Italian
"pt_BR", // Brazilian Portuguese
]);

/** Source string to translated string */
type Translations = Map<string, string>;

/** File names to translation maps */
let fileTranslations = new Map<string, Translations>();

/**
* Parses and stores the actual translations of a given XML file.
* @param xmlNode The XmlDocument node that represents this translation file
* @param locale The locale for this translation file
*/
function parseTranslations(xmlNode: XmlDocument, locale: string) {
let translations: Translations = new Map();
let contextNode = xmlNode.childNamed("context");
contextNode?.childrenNamed("message").forEach((messageElement) => {
let source = messageElement.childNamed("source");
let translation = messageElement.childNamed("translation");
if (source && translation) {
translations.set(source.val, translation.val);
}
});

fileTranslations.set(locale, translations);
}

/**
* Finds and records all of the translation files that exist in the locale/ folder.
* @param rootDir The root package directory
*/
export async function loadTranslationFiles(interpreter: Interpreter, rootDir: string) {
locales.forEach(async (locale) => {
const filePath = path.join(rootDir, "locale", locale, "translations.ts");
if (fs.existsSync(filePath)) {
let xmlNode: XmlDocument;
try {
let contents = await readFile(filePath, "utf-8");
let xmlStr = contents.toString().replace(/\r?\n|\r/g, "");
xmlNode = new XmlDocument(xmlStr);
} catch (err) {
interpreter.stderr.write(`Error reading translations file ${filePath}: ${err}`);
return;
}

parseTranslations(xmlNode, locale);
}
});
}

export const Tr = new Callable("Tr", {
lkipke marked this conversation as resolved.
Show resolved Hide resolved
signature: {
returns: ValueKind.String,
args: [new StdlibArgument("source", ValueKind.String)],
},
impl: (_: Interpreter, source: BrsString) => {
let locale = RoDeviceInfo.locale;
let translationFile = fileTranslations.get(locale);
let translatedString = translationFile?.get(source.value);

if (translatedString) {
return new BrsString(translatedString);
}

// If there was no translation found, RBI returns the input string.
return source;
},
});
1 change: 1 addition & 0 deletions src/stdlib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from "./GlobalUtilities";
export * from "./CreateObject";
export * from "./File";
export * from "./Json";
export * from "./Localization";
export * from "./Math";
export * from "./Print";
export { Run } from "./Run";
Expand Down
32 changes: 32 additions & 0 deletions test/e2e/Localization.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { execute } = require("../../lib");
const { createMockStreams, resourceFile, allArgs } = require("./E2ETests");

describe("Localization", () => {
let outputStreams;

beforeAll(() => {
outputStreams = createMockStreams();
outputStreams.root = __dirname + "/resources";
});

afterEach(() => {
jest.resetAllMocks();
});

afterAll(() => {
jest.restoreAllMocks();
});

test("components/localization/main.brs", async () => {
await execute([resourceFile("components", "localization", "source", "main.brs")], {
...outputStreams,
root: "test/e2e/resources/components/localization",
});

expect(allArgs(outputStreams.stdout.write).filter((arg) => arg !== "\n")).toEqual([
"Bonjour",
"hello",
"Au revoir",
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.0" language="en_US" sourcelanguage="en_US">
<defaultcodec>UTF-8</defaultcodec>
<context>
<name>default</name>
<message>
<source>Hello</source>
<translation>Bonjour</translation>
</message>
<message>
<source>Fare thee well</source>
<translation>Au revoir</translation>
</message>
</context>
</TS>
6 changes: 6 additions & 0 deletions test/e2e/resources/components/localization/source/main.brs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sub main()
_brs_.process.setLocale("en_US")
print tr("Hello") ' => Bonjour
print tr("hello") ' => hello
lkipke marked this conversation as resolved.
Show resolved Hide resolved
print tr("Fare thee well") ' => Au revoir
end sub