-
Notifications
You must be signed in to change notification settings - Fork 0
Add localization system #1
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
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| /** | ||
| * Build-time localization runtime. | ||
| * | ||
| * The catalog holds the translated strings for exactly one language and is | ||
| * assigned at startup by a generated per-language file (`loc.g.ts`) in the | ||
| * consuming app. English strings are used as both the catalog keys and the | ||
| * fallback: an English build ships no table at all, so `ui.loc` and friends | ||
| * become the identity function and simply return the source English string. | ||
| * | ||
| * One language ships per build image. To localize an app, generate a | ||
| * `loc.g.ts` that assigns `_loc.table` (the id-to-string catalog) and, | ||
| * optionally, `_loc.defaultFont` (a language-specific bitmap font). Build one | ||
| * image per language. | ||
| * | ||
| * Catalog keys are the English source strings. Strings that need different | ||
| * translations depending on where they appear use a `"context#string"` key | ||
| * (see `ui.locc`) so the same English word can map to different translations. | ||
| */ | ||
| namespace _loc { | ||
| /** | ||
| * Translation catalog for the shipped language, assigned by the app's | ||
| * generated `loc.g.ts`. `undefined` means an English build with no table, | ||
| * in which case localization is the identity function. | ||
| */ | ||
| export let table: { [key: string]: string } = undefined | ||
|
|
||
| /** | ||
| * Per-language default bitmap font, assigned by the app's generated | ||
| * `loc.g.ts`. `undefined` means the built-in `bitmaps.font8`. | ||
| */ | ||
| export let defaultFont: ui.TextFont = undefined | ||
| } | ||
|
|
||
| namespace ui { | ||
| /** | ||
| * Localizes an English source string. | ||
| * | ||
| * With no catalog (an English build) the source string is returned | ||
| * unchanged. Otherwise the catalog is looked up by the source string; a | ||
| * missing entry falls back to the source string. | ||
| */ | ||
| export function loc(s: string): string { | ||
| if (!_loc.table) return s | ||
| const entry = _loc.table[s] | ||
| if (entry === undefined) return s | ||
| return entry | ||
| } | ||
|
|
||
| /** | ||
| * Localizes an English source string that needs a context-specific | ||
| * translation. | ||
| * | ||
| * The catalog is looked up by the composite key `context + "#" + s`, which | ||
| * lets the same English word (for example "on" as a relay state versus | ||
| * elsewhere) map to different translations. On a miss the lookup falls back | ||
| * to the plain-string translation via `loc(s)`, and finally to the English | ||
| * source string. With no catalog the source string is returned unchanged. | ||
| */ | ||
| export function locc(context: string, s: string): string { | ||
| if (!_loc.table) return s | ||
| const entry = _loc.table[context + "#" + s] | ||
| if (entry === undefined) return loc(s) | ||
| return entry | ||
| } | ||
|
|
||
| /** | ||
| * Localizes an English source string with positional interpolation. | ||
| * | ||
| * The source string is localized first, then each `"{i}"` token is | ||
| * replaced with `args[i]`. Because translation happens before substitution, | ||
| * a translated string may reorder the placeholders (for example map | ||
| * "LED {0} {1}" to "{1} {0} DEL"). | ||
| */ | ||
| export function locf(s: string, args: string[]): string { | ||
| let result = loc(s) | ||
| for (let i = 0; i < args.length; i++) { | ||
| result = result.replaceAll("{" + i + "}", args[i]) | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| /** | ||
| * Returns the default bitmap font for the shipped language, or | ||
| * `bitmaps.font8` when no language-specific font is set. | ||
| */ | ||
| export function locFont(): TextFont { | ||
| if (_loc.defaultFont) return _loc.defaultFont | ||
| return bitmaps.font8 | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| // Regenerates locales/en.json from source. | ||
| // | ||
| // Scans the .ts files listed in pxt.json "files" (so test files are excluded) | ||
| // and extracts the source display strings passed to the localization helpers: | ||
| // | ||
| // ui.loc("...") / loc("...") | ||
| // ui.locf("...") / locf("...") (first argument only) | ||
| // ui.locc("ctx", "...") / locc("ctx", "...") (emits key "ctx#string") | ||
| // | ||
| // Arguments must be string literals at the call site (the documented | ||
| // convention; see locales/README.md). Any other argument is reported as a | ||
| // loud warning naming the file and the argument, so a missed string is never | ||
| // dropped silently. A call whose argument is deliberately dynamic (its | ||
| // strings reach the catalog by another route) is suppressed by putting | ||
| // "locstrings-ignore" in a comment on the call's line. | ||
| // | ||
| // The result is a sorted JSON object mapping each catalog key to its source | ||
| // display string. For locc entries the value is the plain string, without the | ||
| // context prefix. The file is fully derived: it is regenerated from source, so | ||
| // any prior keys not found in the current sources are dropped. | ||
|
|
||
| import { readFileSync, writeFileSync } from "node:fs" | ||
| import { dirname, join } from "node:path" | ||
| import { fileURLToPath } from "node:url" | ||
|
|
||
| const root = join(dirname(fileURLToPath(import.meta.url)), "..") | ||
|
|
||
| // A double- or single-quoted string literal, honoring escaped quotes. | ||
| const STR = `"(?:[^"\\\\]|\\\\.)*"|'(?:[^'\\\\]|\\\\.)*'` | ||
| // One helper argument: a string literal, or any run up to the next comma or | ||
| // close paren. The string alternative is tried first so commas or parens | ||
| // inside a literal do not truncate the capture. | ||
| const ARG = `(?:${STR}|[^,)]*)` | ||
|
|
||
| // Guards for false call-site matches: a helper definition (`function | ||
| // loc(...)`) is not a call, and a preceding identifier character or dot means | ||
| // the name is a property access (obj.loc) or a longer identifier (myloc), | ||
| // not the helper. | ||
| const DEF = `(?<!\\bfunction\\s{1,8})(?<![$\\w.])` | ||
| // loc(...) and locf(...): first argument. The optional "f" plus the required | ||
| // "(" lookahead avoids matching locc and locFont. | ||
| const LOC_RE = new RegExp(`${DEF}(?:ui\\.)?locf?\\s*\\(\\s*(${ARG})`, "g") | ||
| // locc("ctx", "..."): both arguments. | ||
| const LOCC_RE = new RegExp( | ||
| `${DEF}(?:ui\\.)?locc\\s*\\(\\s*(${ARG})\\s*,\\s*(${ARG})`, | ||
| "g", | ||
| ) | ||
|
|
||
| const STR_ONLY = new RegExp(`^(?:${STR})$`) | ||
|
|
||
| // Removes line and block comments so commented-out code is neither extracted | ||
| // nor reported as an unresolved argument. String and template literals are | ||
| // copied verbatim so a "//" or "/*" inside a literal is preserved. | ||
| function stripComments(src) { | ||
| let out = "" | ||
| let i = 0 | ||
| const n = src.length | ||
| while (i < n) { | ||
| const c = src[i] | ||
| const d = src[i + 1] | ||
| if (c == "/" && d == "/") { | ||
| i += 2 | ||
| while (i < n && src[i] != "\n") i++ | ||
| continue | ||
| } | ||
| if (c == "/" && d == "*") { | ||
| i += 2 | ||
| while (i < n && !(src[i] == "*" && src[i + 1] == "/")) i++ | ||
| i += 2 | ||
| continue | ||
| } | ||
| if (c == '"' || c == "'" || c == "`") { | ||
| out += c | ||
| i++ | ||
| while (i < n) { | ||
| const ch = src[i] | ||
| if (ch == "\\") { | ||
| out += ch + (i + 1 < n ? src[i + 1] : "") | ||
| i += 2 | ||
| continue | ||
| } | ||
| out += ch | ||
| i++ | ||
| if (ch == c) break | ||
| } | ||
| continue | ||
| } | ||
| out += c | ||
| i++ | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| // Turns a source string literal into its runtime string value. | ||
| function unquote(lit) { | ||
| const body = lit.slice(1, -1) | ||
| return body.replace(/\\(.)/g, (_, ch) => { | ||
| if (ch == "n") return "\n" | ||
| if (ch == "t") return "\t" | ||
| if (ch == "r") return "\r" | ||
| return ch | ||
| }) | ||
| } | ||
|
|
||
| let warnings = 0 | ||
|
|
||
| // Resolves a captured helper argument to its string value, or null (with a | ||
| // warning) when it is not a string literal. | ||
| function resolveArg(raw, file) { | ||
| const arg = raw.trim() | ||
| if (arg == "") return null | ||
| if (STR_ONLY.test(arg)) return unquote(arg) | ||
| console.warn(`locstrings: WARNING: ${file}: cannot resolve loc argument '${arg}'`) | ||
| warnings++ | ||
| return null | ||
| } | ||
|
|
||
| const pxt = JSON.parse(readFileSync(join(root, "pxt.json"), "utf8")) | ||
| const catalog = {} | ||
|
|
||
| // The file that implements the localization API passes its own parameters | ||
| // through loc(); it defines the helpers rather than consuming them, so it is | ||
| // not a source of catalog keys. | ||
| const API_DEF_RE = new RegExp(`\\bexport\\s+function\\s+loc\\s*\\(`) | ||
|
|
||
| for (const file of pxt.files) { | ||
| if (!file.endsWith(".ts")) continue | ||
| const raw = readFileSync(join(root, file), "utf8") | ||
| .split("\n") | ||
| .filter(line => !line.includes("locstrings-ignore")) | ||
| .join("\n") | ||
| const src = stripComments(raw) | ||
| if (API_DEF_RE.test(src)) continue | ||
|
|
||
| for (const m of src.matchAll(LOCC_RE)) { | ||
| const ctx = resolveArg(m[1], file) | ||
| const str = resolveArg(m[2], file) | ||
| if (ctx == null || str == null) continue | ||
| catalog[ctx + "#" + str] = str | ||
| } | ||
| for (const m of src.matchAll(LOC_RE)) { | ||
| const str = resolveArg(m[1], file) | ||
| if (str == null) continue | ||
| catalog[str] = str | ||
| } | ||
| } | ||
|
|
||
| const sorted = {} | ||
| for (const key of Object.keys(catalog).sort()) sorted[key] = catalog[key] | ||
|
|
||
| writeFileSync( | ||
| join(root, "locales", "en.json"), | ||
| JSON.stringify(sorted, null, 4) + "\n", | ||
| ) | ||
|
|
||
| const count = Object.keys(sorted).length | ||
| console.log(`locstrings: wrote ${count} key(s) to locales/en.json`) | ||
| if (warnings) console.log(`locstrings: ${warnings} unresolved loc argument(s); see warnings above`) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.