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

Add a --garble option to localebuilder. #1468

Merged
merged 11 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
57 changes: 51 additions & 6 deletions frontend/localebuilder/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
import { argvHasOption } from "../querybuilder/util";
import { checkExtractedMessagesSync } from "./check-extracted-messages";
import { assertNotUndefined } from "../lib/util/util";
import PO from "pofile";
import { garbleMessage, Garbler } from "./garble";

const MY_DIR = __dirname;

Expand Down Expand Up @@ -57,19 +59,21 @@ const SPLIT_CHUNK_CONFIGS: MessageCatalogSplitterChunkConfig[] = [
},
];

function readTextFileSync(path: string): string {
return fs.readFileSync(path, {
encoding: "utf-8",
});
}

/**
* Split up the message catalog for a single locale.
*/
function processLocale(paths: MessageCatalogPaths, validate: boolean) {
console.log(`Processing locale '${paths.locale}'.`);

const messagesJs = fs.readFileSync(paths.js, {
encoding: "utf-8",
});
const messagesJs = readTextFileSync(paths.js);
const compiled = parseCompiledMessages(messagesJs);
const messagesPo = fs.readFileSync(paths.po, {
encoding: "utf-8",
});
const messagesPo = readTextFileSync(paths.po);
var extracted = parseExtractedMessages(messagesPo);
if (validate) {
extracted.validateIdLengths(MAX_ID_LENGTH);
Expand All @@ -82,6 +86,41 @@ function processLocale(paths: MessageCatalogPaths, validate: boolean) {
splitter.split();
}

const defaultGarbler: Garbler = (text) => {
return text.replace(/[A-Za-z]/g, "?");
};

function garbleMessageCatalogs(
allPaths: MessageCatalogPaths[],
defaultPaths: MessageCatalogPaths,
garbler: Garbler = defaultGarbler
) {
const defaultPo = PO.parse(readTextFileSync(defaultPaths.po));
const sources = new Map<string, string>();

for (let item of defaultPo.items) {
sources.set(item.msgid, garbleMessage(garbler, item.msgstr.join("")));
}

for (let paths of allPaths) {
if (paths === defaultPaths) continue;
const localePo = PO.parse(readTextFileSync(paths.po));

for (let item of localePo.items) {
const garbled = sources.get(item.msgid);
if (!garbled) {
throw new Error(
`${defaultPaths.locale} source not found for msgid "${item.msgid}"!`
);
}
item.msgstr = [garbled];
}

console.log(`Garbling ${paths.po}.`);
fs.writeFileSync(paths.po, localePo.toString(), { encoding: "utf-8" });
}
}

/**
* Main function to run the localebuilder CLI.
*/
Expand All @@ -90,6 +129,7 @@ export function run() {
console.log(`usage: ${process.argv[1]} [OPTIONS]`);
console.log(`options:\n`);
console.log(" --check Ensure PO files are up to date");
console.log(" --garble Enact gobbledygook translation");
console.log(" -h / --help Show this help");
console.log(" -v / --version Show the version number");
process.exit(0);
Expand All @@ -107,6 +147,11 @@ export function run() {
process.exit(0);
}

if (argvHasOption("--garble")) {
garbleMessageCatalogs(allPaths, defaultPath);
process.exit(0);
}

let validate = true;
for (let paths of allPaths) {
processLocale(paths, validate);
Expand Down
126 changes: 126 additions & 0 deletions frontend/localebuilder/garble.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* A function that takes a fragment of English text and garbles
* it in some way.
*/
export type Garbler = (text: string) => string;

/**
* Take the raw string from a message catalog and "garble" its English
* text into gobbledygook, while preserving all code.
*/
export function garbleMessage(garbler: Garbler, source: string): string {
const s = new GarblerState(garbler, source);
handleEnglish(s);
return s.value;
}

class GarblerState implements Iterator<string> {
private parts: string[] = [];
private i: number = 0;
private substringStartIndex: number = 0;

constructor(readonly garbler: Garbler, readonly source: string) {}

private get hasSubstring() {
return this.i - this.substringStartIndex > 0;
}

private get substring() {
return this.source.substring(this.substringStartIndex, this.i);
}

private push(value: string) {
this.parts.push(value);
this.substringStartIndex = this.i;
}

pushEnglish() {
this.hasSubstring && this.push(this.garbler(this.substring));
}

pushCode() {
this.hasSubstring && this.push(this.substring);
}

backtrack() {
this.i--;
}

next() {
if (this.i === this.source.length) {
return { value: "", done: true };
}
const value = this.source[this.i];
this.i++;
return { value, done: false };
}

pushCodeUntil(chars: string) {
for (let ch of this) {
if (chars.indexOf(ch) !== -1) {
this.pushCode();
this.next();
return;
}
}

this.pushCode();
}

[Symbol.iterator]() {
return this;
}

get value() {
return this.parts.join("");
}
}

type StateHandler = (s: GarblerState) => void;

type StateHandlerMap = {
[ch: string]: StateHandler | undefined;
};

const handleEnglish = (s: GarblerState, untilChar?: string) => {
const handlers: StateHandlerMap = {
"{": handleVariable,
"<": handleTag,
};

for (let ch of s) {
if (ch === untilChar) {
s.backtrack();
s.pushEnglish();
return;
}

let newHandler = handlers[ch];

if (newHandler) {
s.backtrack();
s.pushEnglish();
newHandler(s);
s.backtrack();
}
}

s.pushEnglish();
};

const handleVariable: StateHandler = (s) => {
s.next();
for (let ch of s) {
if (ch === "{") {
s.pushCode();
handleEnglish(s, "}");
s.next();
} else if (ch === "}") {
s.pushCode();
s.next();
return;
}
}
};

const handleTag: StateHandler = (s) => s.pushCodeUntil(">");
39 changes: 39 additions & 0 deletions frontend/localebuilder/tests/garble.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { garbleMessage, Garbler } from "../garble";

const wordsToXs: Garbler = (text) => {
return text
.split(" ")
.map((word) => (word ? "X" : ""))
.join(" ");
};

describe("garbleMessage()", () => {
const garble = garbleMessage.bind(null, wordsToXs);

it("works with simple strings", () => {
expect(garble("Hello world")).toBe("X X");
});

it("Doesn't garble tags", () => {
expect(garble("<0>Hello world</0> <1/>")).toBe("<0>X X</0> <1/>");
});

it("Doesn't garble variables", () => {
expect(garble("Hello {firstName} how goes")).toBe("X {firstName} X X");
});

it("Doesn't garble variables in tags", () => {
expect(garble("Hello <0>{firstName}</0> how goes")).toBe(
"X <0>{firstName}</0> X X"
);
});

it("Doesn't garble plurals", () => {
expect(
garble(
"Marshals scheduled {totalEvictions, plural, one {one eviction} " +
"other {# evictions}} across this portfolio."
)
).toBe("X X {totalEvictions, plural, one {X X} other {X X}} X X X");
});
});