Skip to content
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
5 changes: 4 additions & 1 deletion content/playground-how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ flowchart TB
G --> I["uutils.wasm<br/>(compileStreaming)"]
H --> J["Ready"]
I --> J
J --> K{"?cmd= parameter?"}
J --> K0{"?lang= parameter?"}
K0 -->|Yes| K1["Set locale (LANG=…)"]
K1 --> K{"?cmd= parameter?"}
K0 -->|No| K
K -->|Yes| L["Auto-run commands"]
K -->|No| M["Show prompt"]
</pre>
Expand Down
9 changes: 9 additions & 0 deletions content/playground.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ Multiple commands can be run in sequence, separated either by `;` on one line or
- [`?cmd=echo hello%0Aecho world`](/playground?cmd=echo%20hello%0Aecho%20world) - run two commands in sequence
- [`?cmd=updatedb; locate names`](/playground?cmd=updatedb%3B%20locate%20names) - build the locate database, then search it

Add `?lang=` to run the command in another language - it sets the locale the
same way the **Language** dropdown does, before the command runs. Both the full
form (`fr-FR`) and the short form (`fr`) work, and an unknown language is
ignored. The share-link button includes it automatically when the locale isn't
the default:

- [`?lang=fr-FR&cmd=cut -f 1,4-2,9-12 fruits.txt`](/playground?lang=fr-FR&cmd=cut%20-f%201%2C4-2%2C9-12%20fruits.txt) - a French error message
- [`?lang=de&cmd=ls /nope`](/playground?lang=de&cmd=ls%20%2Fnope) - a German error message

## Available commands

The following commands run as **real Rust coreutils compiled to WebAssembly**:
Expand Down
26 changes: 23 additions & 3 deletions static/js/playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ document.addEventListener("DOMContentLoaded", function() {
var url = new URL(window.location.href);
url.search = "";
url.hash = "";
// Only carry the locale when it isn't the default, to keep links short.
var locale = window.getLocale ? window.getLocale() : "";
if (locale && locale !== "en-US") url.searchParams.set("lang", locale);
url.searchParams.set("cmd", cmd);
return url.toString();
};
Expand Down Expand Up @@ -103,17 +106,34 @@ document.addEventListener("DOMContentLoaded", function() {
}

// Populate the locale dropdown from the build-generated list
if (typeof WASM_LOCALES !== "undefined") {
var sel = document.getElementById("locale-select");
var localeSelect = document.getElementById("locale-select");
if (typeof WASM_LOCALES !== "undefined" && localeSelect) {
WASM_LOCALES.forEach(function(loc) {
if (loc === "en-US") return; // already the default option
var opt = document.createElement("option");
opt.value = loc;
opt.textContent = loc;
sel.appendChild(opt);
localeSelect.appendChild(opt);
});
}

// Keep the dropdown in sync when the locale is set elsewhere: the `locale`
// builtin, or the ?lang= URL parameter on load.
document.addEventListener("uutils:locale-changed", function(e) {
if (!localeSelect || !e.detail) return;
var loc = e.detail.locale;
var known = Array.prototype.some.call(localeSelect.options, function(o) {
return o.value === loc;
});
if (!known) {
var opt = document.createElement("option");
opt.value = loc;
opt.textContent = loc;
localeSelect.appendChild(opt);
}
localeSelect.value = loc;
});

// Populate the "Available commands" list from the build-generated list
if (typeof WASM_COMMANDS !== "undefined" && Array.isArray(WASM_COMMANDS)) {
var listEl = document.getElementById("wasm-commands-list");
Expand Down
60 changes: 56 additions & 4 deletions static/js/wasm-terminal.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,37 @@ function sanitizeUrlCommand(raw) {
return cmd;
}

/**
* Normalize a locale name: "fr" -> "fr-FR", "en" -> "en-US", full forms as-is.
*/
function normalizeLocale(raw) {
const arg = (raw || "").trim();
if (!arg) return "";
return arg.includes("-") ? arg : LOCALE_SHORTCUTS[arg.toLowerCase()] || arg;
}

/**
* Sanitize a locale coming from the ?lang= URL parameter.
*
* Accepts either a shortcut ("fr") or a full locale ("fr-FR"), and only
* returns a value the build actually ships (WASM_LOCALES, when available) so
* a bogus ?lang= cannot push an arbitrary string into the LANG environment
* variable handed to the WASM runtime. Returns "" when unusable.
*/
function sanitizeUrlLocale(raw) {
const locale = normalizeLocale(raw);
if (!locale || !/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})?$/.test(locale)) return "";
const available =
(typeof WASM_LOCALES !== "undefined" && Array.isArray(WASM_LOCALES) && WASM_LOCALES.length > 0)
? WASM_LOCALES
: null;
if (available) {
const match = available.find(l => l.toLowerCase() === locale.toLowerCase());
return match || "";
}
return locale;
}

/**
* Read a file from the virtual filesystem. Returns its content as a string,
* or null if not found.
Expand Down Expand Up @@ -698,9 +729,8 @@ async function executeSingleCommandLine(line) {
if (!arg) {
return `LANG=${currentLocale}.UTF-8\n`;
}
// Normalize: "fr" -> "fr-FR", "en" -> "en-US", or accept full form
const normalized = arg.includes("-") ? arg : LOCALE_SHORTCUTS[arg.toLowerCase()] || arg;
currentLocale = normalized;
currentLocale = normalizeLocale(arg);
notifyLocaleChanged();
return `Locale set to ${currentLocale}\n`;
}

Expand Down Expand Up @@ -1112,8 +1142,18 @@ async function initPlayground(containerId) {
terminal.writeln("Try reloading the page.");
}

// Apply the locale from the URL ?lang= parameter before running ?cmd=, so a
// shared link like ?lang=fr-FR&cmd=... shows the localized output.
const params = new URLSearchParams(window.location.search);
const urlLocale = sanitizeUrlLocale(params.get("lang"));
if (urlLocale) {
currentLocale = urlLocale;
notifyLocaleChanged();
terminal.writeln(`Locale set to ${currentLocale}`);
}

// Run command(s) from URL ?cmd= parameter if present
const urlCmd = sanitizeUrlCommand(new URLSearchParams(window.location.search).get("cmd"));
const urlCmd = sanitizeUrlCommand(params.get("cmd"));
if (urlCmd) {
for (const cmd of urlCmd.split("\n")) {
if (cmd.trim()) await runInTerminal(cmd.trim());
Expand All @@ -1137,11 +1177,22 @@ async function runInTerminal(cmd) {
prompt();
}

/**
* Let the page chrome (locale dropdown, share link) know the locale changed,
* whichever way it was set: dropdown, `locale` builtin or ?lang= URL param.
*/
function notifyLocaleChanged() {
document.dispatchEvent(new CustomEvent("uutils:locale-changed", {
detail: { locale: currentLocale },
}));
}

/**
* Set the locale and optionally update the terminal.
*/
function setLocale(locale) {
currentLocale = locale;
notifyLocaleChanged();
if (terminal) {
terminal.writeln(`\r\nLocale set to ${currentLocale}`);
prompt();
Expand All @@ -1154,6 +1205,7 @@ window.uutilsExecute = executeCommandLine;
window.runInTerminal = runInTerminal;
window.setLocale = setLocale;
window.getLastCommand = () => lastCommand;
window.getLocale = () => currentLocale;

// On-demand loading of the optional standalone modules, used by the "Load"
// buttons on the playground page. Buttons operate on groups (see
Expand Down