diff --git a/.github/workflows/verso-ci.yml b/.github/workflows/verso-ci.yml index e6280562..80a860e9 100644 --- a/.github/workflows/verso-ci.yml +++ b/.github/workflows/verso-ci.yml @@ -9,6 +9,7 @@ on: - "templates/**" - "vscode/**" - "samples/showcase/**" + - "build/i18n/**" - "Verso.sln" - ".github/workflows/verso-ci.yml" pull_request: @@ -19,6 +20,7 @@ on: - "templates/**" - "vscode/**" - "samples/showcase/**" + - "build/i18n/**" - "Verso.sln" - ".github/workflows/verso-ci.yml" workflow_dispatch: @@ -58,6 +60,14 @@ jobs: - name: Install Python test dependencies run: python -m pip install --upgrade pip jedi + # None of what this catches fails the build: a missing translation falls back to English, + # and a translation that lost the `{0}` it was going to fill in only throws when the message + # is finally shown to somebody. It reads the committed files and nothing else, so it needs no + # API key and no network, and it runs before the build so a drifted string is reported in + # seconds rather than after everything has compiled. + - name: Check translations + run: python build/i18n/check.py + - name: Restore run: dotnet restore Verso.sln diff --git a/.gitignore b/.gitignore index 6d4d9d06..f4eb0a2d 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,7 @@ vscode/blazor-wasm/ __pycache__/ *.pyc .pytest_cache/ + +## Translation handoff files — working files passed to and from a translator. +## The finished translations live in the resource files and are committed. +build/i18n/pending/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4a09950..da62ed58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,6 +73,36 @@ By making a contribution to this project, I certify that: this project or the open source license(s) involved. ``` +## Writing a Message + +Verso's interface is translated, so a new string usually belongs in a resource file rather than +in the code that shows it. `build/i18n/README.md` covers where each one goes and how to add it. + +The question worth asking first is who reads it. A message is translated when the person reading +it can do something about it: a package that could not be downloaded, a parameter whose value does +not fit its type, a connection that has closed. Those go in a resource file with a note saying +where they appear. + +Four kinds stay in English wherever they appear, and each is marked with a comment in the code +saying so: + +- **Guards against programmer error.** `ArgumentNullException`, an internal + `InvalidOperationException`, a check that a method was called before the one that sets it up. + Nobody reading one can act on it except by changing code, and a stack trace that matches an + issue report is worth more than a translated one that does not. +- **Protocol shape.** The host answers the editor over a small JSON-RPC surface, and a request + missing a field it requires is a fault in the caller, not something a reader chose. Those read + the same in every language so a log from one machine matches a search from another. +- **Anything a script reads rather than a person.** The tags a run writes on its error stream, + and the status values in the document `--output json` produces. +- **Anything a model reads rather than a person.** The chat participant's prompt and the + descriptions of the tools it can call. + +Two shapes to avoid whatever the string says. Do not build a word from a stem and a letter +(`cell(s)`, `row(s)`, `entit{y|ies}`); write the two forms as separate entries and pick between +them with `Plural.Of`. Do not assemble a sentence from fragments; write it whole with numbered +placeholders, because another language will not put the pieces in that order. + ## Pull Requests - Target the `main` branch. diff --git a/README.md b/README.md index b857d7ca..f614d69b 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,10 @@ Markdown (rendered via Markdig), raw HTML, and Mermaid diagram cells all support Three built-in themes (Light, Dark, High Contrast) are hot-swappable at runtime. The High Contrast theme meets WCAG 2.1 AA contrast requirements. In VS Code, the notebook theme automatically follows your editor theme. +### Interface Language + +The notebook interface, the toolbar and panels, the kernel messages that land in cell output, and the CLI are translated into German, Spanish, Japanese, and Simplified Chinese. Verso follows the system or the editor on its own; `verso.language` and `--language` override it, and `VERSO_LANGUAGE` sets it once for a container or a pipeline. Only the words change: numbers and dates keep the machine's own formatting, so a language never alters what a cell computes. See the [interface language guide](docs/guides/interface-language.md). + ### GitHub Copilot Integration In VS Code, a `@verso` chat participant answers questions about the notebook in front of you, and twenty language model tools let agent mode create, edit, run, and inspect cells directly. Copilot works against the real notebook rather than a text approximation of it. diff --git a/build/i18n/README.md b/build/i18n/README.md new file mode 100644 index 00000000..b7673f3f --- /dev/null +++ b/build/i18n/README.md @@ -0,0 +1,236 @@ +# Translations + +Verso's interface is written in English and translated into German, Spanish, Japanese, and +Simplified Chinese. The translations are committed files, so building Verso and checking +the translations need no API key and no network. + +Strings live in two places. .NET code reads `.resx` files under `src//Resources`, +one set per assembly, which the build turns into a satellite assembly per language. The +editor extension reads `vscode/package.nls.json` for anything named in its manifest and +`vscode/l10n/bundle.l10n.json` for the strings its own code shows. + +One set per assembly is not a choice: a satellite assembly carries one assembly's resources, so a +kernel that ships as its own package needs its own. Each kernel therefore has a `Resources` folder +and a block in its `.csproj` that generates the accessor. `Plural` lives in `Verso.Abstractions`, +which every one of them already references. + +## Which file a string belongs in + +Two languages are in play, and a reader may set them differently. The editor writes its own +menus, commands, and settings in whatever display language its workbench is set to, and no +Verso setting can override that. The notebook interface, the host, and the CLI answer in the +language `verso.language` or `--language` asks for. So the same words can be needed in both +places, and that is not duplication to remove: the Compare command in the palette and the +Compare panel inside a notebook can legitimately be showing two different languages at once. + +The rule that decides it: if the string is drawn by the editor, it belongs in one of the two +JSON bundles; if it is drawn inside a notebook, it belongs in a `.resx`. Where the editor +sends something a notebook will draw, it sends an identifier and the notebook chooses the +words, which is what `DiffSources` does for the comparison baselines. + +Four kinds of string stay in English wherever they appear, and each is marked with a comment +in the code saying so: + +- **Anything read by a model rather than a person.** The chat participant's system prompt, + the tool descriptions in the manifest, and everything the tools hand back. Translating + them changes how well tools are chosen without changing anything a reader sees. +- **Anything read by a script rather than a person.** The `[stderr]` and `[error]` tags a run + writes, and the status values in the document `--output json` produces. A pipeline that reads + those would break the moment the machine running it was set to another language. +- **Text that only a fault produces.** Log lines and guards against programmer error stay + searchable, so a stack trace and an issue report still match. +- **The shape of the protocol.** The host answers the editor over a small JSON-RPC surface, and a + request missing a field it declares is a fault in the caller rather than something the reader + did. A log from one machine has to match a search made on another. + +The line between the last two and everything else is who can act on the message. A package that +would not download, a value that does not fit the type its parameter declares, a connection that +has since closed: the reader can do something about each of those, so each is translated. A cell +id that does not exist, or a request without the field it said it had, is a fault in code. + +## Adding a string + +Add it to the neutral file, in English, with a note saying where it appears and what +constrains it. In a `.resx` that is the `` element. In `package.nls.json` it is the +`{ "message": ..., "comment": [...] }` form. In extension code it is the object form of +`vscode.l10n.t`, and the note travels into the bundle when it is exported: + +```ts +vscode.l10n.t({ message: "tag", comment: ["A name pinned to one point in a project's history."] }) +``` + +That note is the only context a translator gets, and "keep short, it sits next to an icon" +is the difference between a button that fits and one that does not. It matters most for +single words: `Type`, `Value`, and `No` all mean more than one thing on their own. + +After editing extension code, re-export the bundle so the new strings reach a translator: + +``` +cd vscode && npx @vscode/l10n-dev export --outDir ./l10n ./src +``` + +Then translate it, as below, and regenerate the pseudo-locale so the coverage sweep keeps +working. + +## Translating + +Two routes fill in a language, and they write the same files. Both ask only for the keys a +language does not already have, so adding a handful of English strings costs a handful of +translations rather than a retranslation of the interface. + +Read `glossary.md` before either. It is what keeps `kernel` from becoming three different +words in three files, and it lists what must not be translated at all. + +### Handing the strings to a translator + +`export.py` writes out what a language is missing, each string with its English and whatever +note the developer left beside it. Nothing about that file is particular to Verso, so it can +go to a person, to a translation service, or to an assistant in a session. + +``` +python3 build/i18n/export.py de --limit 100 +python3 build/i18n/export.py de --set Verso.Ado/Strings +``` + +The answer comes back through `merge.py`, in the shape its docstring gives: + +``` +python3 build/i18n/merge.py build/i18n/pending/de.answer.json +``` + +A translation that dropped a `{0}`, or came back empty, or answers a key English does not +have, is refused and named rather than written. Everything sound in the same run still lands, +so a rerun only has to cover what was named. + +Export, translate, merge, export again. The second export asks for what is still outstanding +and nothing else, which is what makes a language safe to do over several sittings without +anyone keeping track of where it got to. `pending/` is working files and is not committed. + +### Against the API + +`translate.py` does the whole of that in one command, which is the better route for somebody +outside the project who would rather spend an API key than an afternoon. + +``` +python3 build/i18n/translate.py --locale de +``` + +It needs `pip install anthropic` and `ANTHROPIC_API_KEY`. Nothing in the build or in +continuous integration runs it, so neither building Verso nor checking the translations +needs a key. + +### Either way, afterwards + +``` +python3 build/i18n/pseudo.py # regenerates the pseudo-locale +python3 build/i18n/check.py # confirms the languages agree with the English +``` + +A translation nobody has read is a draft, whichever route produced it. Have somebody who +reads the language look over anything user-facing before it ships. + +## Counting things + +Neither format has plural rules, so a count is written as two entries and the code picks +between them: + +```ts +count === 1 ? vscode.l10n.t("{0} cell", count) : vscode.l10n.t("{0} cells", count) +``` + +Two forms cover German, Spanish, Japanese, and Simplified Chinese; the last two have one +form and translate both entries the same way. A language with more forms than two, such as +Russian or Polish, would need a real plural selector, and that is worth knowing before +adding one. What must not happen is `cell(s)`, which no other language can copy. + +In .NET the pair is chosen through `Plural.Of`. Where the count is dropped into a longer +sentence, write the count out on its own and pass the phrase in as an argument, so the +sentence needs one entry rather than a singular and a plural of the whole thing: + +```csharp +string.Format(Strings.Meta_Save_Done, CellCount.Describe(cells.Count), path) +``` + +## Words and styling + +The CLI writes coloured output through Spectre.Console, whose markup is written in square +brackets. None of it reaches a translator: `Messages` in `src/Verso.Cli/Utilities` fills a +translated sentence in and adds the styling around it, and everything substituted in is +escaped, because a file path can contain a bracket too. + +Where a sentence names something typed at a keyboard, that part is a placeholder rather than +part of the words, so it survives the sentence being rewritten: + +```csharp +Messages.Typed(Strings.Repl_UnsavedHint, ".save", ".load") +``` + +Colour goes on a whole line rather than on a word inside it. English puts the verb first, so +`Saved` could be picked out where it stood; a language that ends with its verb would leave +the colour on whatever happened to come first instead. + +## Sentences built from pieces + +A message that reports a count is the usual place a sentence gets assembled out of fragments, and +the usual place translation breaks. `"Installed " + list + " and " + n + " dependencies."` cannot +be translated at all: every join is a decision about word order that only English made. + +Write the whole sentence as one entry with numbered placeholders, and write any count out on its +own so it goes in as a single argument: + +```csharp +var dependencies = string.Format( + Plural.Of(rest.Count, Strings.Npm_DependencyCount_One, Strings.Npm_DependencyCount_Other), + rest.Count); + +return string.Format(Strings.Npm_InstalledAnd, Describe(named), dependencies); +``` + +Where a message continues on the same line or the next one, each part is still a whole entry +rather than a phrase glued on, and the entry carries its own leading space or line break. Where +something wraps an assembled sentence rather than following it, the wrapper takes the sentence as +its placeholder: `"{0} (execution failed)"` rather than `+ " (execution failed)"`. + +## Text a browser rewrites + +The table a SQL query comes back as repaints its own footer as the reader pages through it, so the +sentence goes into the page as a template with its placeholders intact and the script fills them +in. Assembling it there out of words and numbers would put it beyond a translator's reach, and the +static footer and the moving one would drift apart. + +## Things that line up + +A column heading, a padded label, and a rule drawn to a fixed width are all measured from the +string itself, never from a count written into the code. A translated heading is not the +length the English one was, and a table whose columns no longer line up reads as a fault +rather than as a translation. + +## Adding a language + +1. Add the tag to `VersoCultures.Supported` in `src/Verso/Localization/VersoCultures.cs`. +2. Add it to `LOCALES` and `LANGUAGE_NAMES` in `resources.py`, and to `VSCODE_IDS` if the + editor spells it differently, as it does for Chinese. +3. Add it to the `verso.language` setting's `enum` and `enumItemLabels` in + `vscode/package.json`, and to `SHIPPED` in `vscode/src/localization.ts`. +4. Translate it by either route above, then run `pseudo.py` and `check.py`. + +## Checking the work + +`check.py` is the one to run in continuous integration. It reports a key the English has +and a language does not, a key a language still has after English dropped it, a translation +that lost a `{0}` it was meant to fill in, and an empty translation. It also checks the +editor manifest against `package.nls.json` in both directions, because a `%key%` with +nothing behind it is drawn on screen exactly as written. All of those otherwise stay quiet +until the string is finally shown to somebody. + +The pseudo-locale is the coverage check. Run the interface in `qps-Ploc` and every +translated string appears accented, bracketed, and padded, so anything still in plain +English is a string nobody moved into a resource file, and anything clipped is a place a +real translation will not fit. + +``` +verso serve --language qps-Ploc +``` + +In the editor, set `verso.language` to `qps-Ploc` by hand. It is deliberately absent from +the setting's dropdown, because it is a development aid rather than a language. diff --git a/build/i18n/check.py b/build/i18n/check.py new file mode 100644 index 00000000..46bea6de --- /dev/null +++ b/build/i18n/check.py @@ -0,0 +1,133 @@ +"""Checks the translations against the English strings they came from. + +Translations are committed files, so they drift: a string gets added and only English +knows about it, a key gets renamed and four files keep the old one, a translation quietly +loses the `{0}` that was going to be filled in with a file name. None of that shows up at +build time, because a missing translation falls back to English and a broken placeholder +only fails when the message is finally shown. + + python3 build/i18n/check.py + +Prints what is wrong and exits non-zero, so it can run in continuous integration. It needs +no API key: it reads the committed files and nothing else. +""" + +from __future__ import annotations + +import json +import re +import sys + +from resources import LOCALES, PSEUDO, REPO_ROOT, discover, display, placeholders + +# How the editor's manifest points at a string it wants translated. A reference with no +# entry behind it is drawn on screen exactly as written, braces and all. +NLS_REFERENCE = re.compile(r"^%([^%]+)%$") + + +def check_locale(resource_set, locale: str, source) -> list[str]: + """Every problem found in one language of one resource set.""" + problems: list[str] = [] + path = resource_set.path_for(locale) + + if not path.exists(): + return [f"{display(path)}: missing, {len(source)} strings untranslated"] + + translated = resource_set.translation(locale) + where = display(path) + + for key in sorted(set(source) - set(translated)): + problems.append(f"{where}: missing key {key}") + + # An orphan is usually a rename that only landed in English, and it is worth reporting + # rather than deleting, because the translation it holds may still be wanted under the + # new name. + for key in sorted(set(translated) - set(source)): + problems.append(f"{where}: no longer in English, key {key}") + + for key in sorted(set(source) & set(translated)): + expected = sorted(placeholders(source[key].value)) + actual = sorted(placeholders(translated[key].value)) + if expected != actual: + problems.append( + f"{where}: placeholders differ in {key}, " + f"English has {expected or 'none'} and the translation has {actual or 'none'}" + ) + + if not translated[key].value.strip(): + problems.append(f"{where}: empty translation for {key}") + + return problems + + +def check_manifest() -> list[str]: + """Whether the editor manifest and the strings behind it still agree. + + The manifest names its translatable strings indirectly, and nothing verifies the + naming: a reference with no entry is drawn literally, so a mistyped key shows up as + `%command.newNotebook.title%` in the Command Palette rather than as a failure. An entry + with nothing referencing it is the same mistake seen from the other side, and it also + costs every translator the work of translating a string nobody will read. + """ + manifest = REPO_ROOT / "vscode" / "package.json" + strings = REPO_ROOT / "vscode" / "package.nls.json" + if not manifest.exists() or not strings.exists(): + return [] + + referenced: set[str] = set() + + def walk(node) -> None: + if isinstance(node, dict): + for value in node.values(): + walk(value) + elif isinstance(node, list): + for value in node: + walk(value) + elif isinstance(node, str) and (match := NLS_REFERENCE.match(node)): + referenced.add(match.group(1)) + + walk(json.loads(manifest.read_text(encoding="utf-8"))) + declared = set(json.loads(strings.read_text(encoding="utf-8"))) + + where = display(strings) + return [ + *(f"{where}: {key} is used in package.json but not declared here" + for key in sorted(referenced - declared)), + *(f"{where}: {key} is declared here but nothing in package.json uses it" + for key in sorted(declared - referenced)), + ] + + +def main() -> int: + sets = discover() + if not sets: + print("No neutral resource files found.", file=sys.stderr) + return 1 + + problems: list[str] = check_manifest() + strings = 0 + + for resource_set in sets: + source = resource_set.source() + strings += len(source) + + # The pseudo-locale is checked alongside the real ones. It is generated, so a + # problem there means pseudo.py has not been run since the English changed, which + # is worth catching for the same reason: it is what the coverage sweep runs on. + for locale in [*LOCALES, PSEUDO]: + problems.extend(check_locale(resource_set, locale, source)) + + for problem in problems: + print(problem) + + languages = len(LOCALES) + 1 + if problems: + print(f"\n{len(problems)} problems across {strings} strings in {languages} languages.") + return 1 + + print(f"{strings} strings, {languages} languages, no problems.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/build/i18n/export.py b/build/i18n/export.py new file mode 100644 index 00000000..c1599fcd --- /dev/null +++ b/build/i18n/export.py @@ -0,0 +1,121 @@ +"""Writes out the strings a language is still missing, for somebody to translate. + +`translate.py` does this and the translating in one go, against the API. This does the same +work up to the point where the words are actually chosen, and hands that part to whoever is +reading: an assistant in a session, a translator with a text editor, a translation service. +What comes back goes in through `merge.py`. + + python3 build/i18n/export.py de + python3 build/i18n/export.py de --set Verso.Ado/Strings # one resource set + python3 build/i18n/export.py de --limit 100 # a hundred at a time + python3 build/i18n/export.py de --all # including what is done + +Only the missing strings are asked for, so exporting again after a partial merge asks for +what is still outstanding and nothing else. That is what makes a language safe to do in +passes: run this, translate, merge, run this again. + +The file carries the English and whatever note the developer left, because that note is the +only context a translator gets. It does not carry the glossary, which is a document to read +once rather than a preamble to repeat. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +from resources import LANGUAGE_NAMES, LOCALES, REPO_ROOT, discover, display + +# Working files, not artifacts. Ignored by git, because a half-finished German batch is +# nobody else's business and the finished translations live in the resource files. +PENDING = REPO_ROOT / "build" / "i18n" / "pending" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("locale", choices=LOCALES, help="The language to ask for.") + parser.add_argument( + "--set", + dest="sets", + action="append", + help="Limit to resource sets whose name contains this. Repeatable.", + ) + parser.add_argument( + "--limit", + type=int, + help="Ask for at most this many strings, so a large language can be done in passes.", + ) + parser.add_argument( + "--all", + action="store_true", + help="Include strings that already have a translation.", + ) + parser.add_argument("--out", type=Path, help="Where to write. Defaults to pending/.json") + args = parser.parse_args() + + sets = discover() + if not sets: + print("No neutral resource files found.", file=sys.stderr) + return 1 + + if args.sets: + wanted = [s.lower() for s in args.sets] + sets = [s for s in sets if any(w in s.name.lower() for w in wanted)] + if not sets: + print(f"No resource set matches {', '.join(args.sets)}.", file=sys.stderr) + return 1 + + body: dict[str, dict[str, dict[str, str]]] = {} + total = 0 + + for resource_set in sets: + if args.limit is not None and total >= args.limit: + break + + source = resource_set.source() + existing = resource_set.translation(args.locale) + + entries: dict[str, dict[str, str]] = {} + for key in sorted(source): + if not args.all and key in existing: + continue + if args.limit is not None and total + len(entries) >= args.limit: + break + + entry = {"en": source[key].value} + if source[key].comment: + entry["note"] = source[key].comment + entries[key] = entry + + if entries: + body[resource_set.name] = entries + total += len(entries) + + if not total: + print(f"{LANGUAGE_NAMES[args.locale]} is up to date. Nothing to export.") + return 0 + + out = args.out or (PENDING / f"{args.locale}.json") + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text( + json.dumps( + {"locale": args.locale, "language": LANGUAGE_NAMES[args.locale], "sets": body}, + ensure_ascii=False, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + + for name, entries in body.items(): + print(f" {name}: {len(entries)}") + + print(f"\n{total} strings for {LANGUAGE_NAMES[args.locale]} in {display(out)}") + print("Read build/i18n/glossary.md, then answer with merge.py's shape.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/build/i18n/glossary.md b/build/i18n/glossary.md new file mode 100644 index 00000000..7f128ae9 --- /dev/null +++ b/build/i18n/glossary.md @@ -0,0 +1,283 @@ +# Translation glossary + +Rules for translating Verso's interface. `translate.py` passes this file to the model with +every batch, and a human reviewer should hold a translation to the same rules. + +## Never translate + +These are names, not words. They appear as written in every language. + +- **Verso**, **Verso Notebooks**, **Datafication** +- Kernel and language names: **C#**, **F#**, **Python**, **PowerShell**, **JavaScript**, + **SQL**, **HTTP**, **Markdown**, **Mermaid**, **Blazor**, **Monaco**, **NuGet**, + **Jupyter**, **Git** +- File extensions, exactly as spelled: `.verso`, `.ipynb`, `.dib`, `.md`, `.csx`, `.fsx` +- Magic commands and anything else typed at a keyboard: `#!import`, `#!pip`, `#!restart`, + `#!extension`, `#!connect` +- Identifiers of any kind: extension ids, setting names such as `verso.python.useUv`, + environment variables such as `VERSO_LANGUAGE`, command-line options such as + `--language`, MIME types, HTTP method names +- Anything typed to make something happen: `@verso` addresses the chat assistant, `/props` + is a chat command, `HEAD` and `main` are version control names, `.save` and `.exit` are + REPL commands, `all` and `none` are values a command accepts, `true/false`, `yes/no` and + `1/0` are the words a parameter of that type takes +- Keywords, methods, and header names that a specification defines: `SELECT`, `GROUP BY`, + `WHERE`, `LIMIT`, `GET`, `POST`, `Content-Type`, `Accept-Language`. Several entries open + with one and then explain it; explain it in the target language, and leave the word itself +- Command-line switches exactly as spelled: `--name`, `--connection-string`, `--show-output`, + `--list`. The words after them are prose and are translated +- Environment variable names: `VERSO_PYTHON`, `VERSO_LANGUAGE`, `PATH`, `NODE_PATH` +- Package, module, and library names: `typescript`, `pandas`, `numpy`, `npm`, `pip`, `uv`, + `FSharp.Compiler.Service`, `System.Management.Automation`, `Microsoft.Data.SqlClient` +- Argument names as the help text spells them: ``, ``, `..`, + `name=value`. They describe what to type, so they are read alongside what was typed +- The names languages call themselves. A language picker lists **English**, **Deutsch**, + **Español**, **日本語**, **简体中文**, and those read the same whichever language the + picker is in. Only the entry meaning "take it from the editor" is a word to translate. + +## Placeholders + +A placeholder is filled in at runtime. Reproduce every one exactly, spelling and case +included. + +- `{0}`, `{1}` are positional. Move them wherever the sentence needs them, but keep all of + them and add none. +- `{name}` is named. Same rule, and never translate the name inside the braces. +- `{{` and `}}` are an escaped literal brace. Leave them doubled. + +## House terms + +Translate these consistently. Where a target language has an established computing term, +prefer it over a coinage; where the English word is what practitioners actually say in that +language, keep the English word. + +| English | Meaning in Verso | +|---|---| +| notebook | The document: cells, outputs, and metadata in one file | +| cell | One unit of the notebook, holding code or prose | +| output | What a cell produced when it ran | +| kernel | The process that runs a cell's code | +| run / execute | Both mean starting a cell. Use one word consistently | +| extension | An add-on that contributes a kernel, formatter, or panel | +| panel | A dockable region of the interface | +| layout | The arrangement a notebook is rendered with | +| parameter | A named value a notebook declares and a caller supplies | +| variable | A value produced by running a cell | +| trust | The user's decision to let something run | +| meta-command | A dot-prefixed command the REPL answers itself, such as `.save` | +| magic command | A `#!`-prefixed directive a cell can carry, such as `#!pip` | +| session | One run of the REPL, and the notebook it is building | +| package | Something installed from an index: a NuGet package, a Python distribution, an npm module | +| dependency | A package that came along with one that was asked for | +| interpreter | The Python installation a notebook's cells run in | +| connection | A named, open link to a database that SQL cells run against | +| result set | The rows one query came back with | +| schema | What a database holds: its tables, views, and columns. Also the grouping a table belongs to, which is the column heading `Schema` | + +### Naming the things in the Extensions panel + +Every loaded part of Verso is listed there by name, and those names are translated: a reader +should not meet a list half in their language. Three shapes recur, and each keeps its identifier +exactly as it is typed while translating the words around it. + +- A magic command is named for the word it answers to, which is not translated: `Import Magic + Command` becomes `Import-Magic-Command`, `Comando mágico Import`, `Import マジックコマンド`, + `Import 魔法命令`. +- A part of the product is named for what it does: `SQL Renderer`, `Result Set Formatter`, + `Jupyter Serializer`. Language names, format names, and file extensions inside them stay as + written, and only the role word is translated. +- A consent reason says why the dialog is asking, in lower case, because it is drawn in brackets + after the package name. `import cv2` is the exception: it quotes a line of Python back to the + reader, so `import` is a keyword rather than a word. + +## Terms already chosen + +Where a house term above has been settled in a language, it is recorded here so the next batch +matches the last one rather than deciding again. A reviewer who disagrees should change the term +everywhere and update this table, not just the entry in front of them. + +### German + +| English | German | Why | +|---|---|---| +| notebook | Notebook | What German practitioners say. Not Notizbuch, which is the paper kind | +| cell | Zelle | | +| output | Ausgabe | | +| kernel | Kernel | Plural is also Kernel | +| extension | Erweiterung | Matches the editor's own German | +| panel | Panel | Matches the editor's own German | +| theme | Design | The editor calls it Design, so a reader meets the same word twice | +| layout | Layout | | +| baseline | Basis | Also Basisversion where a version rather than a file is meant | +| package | Paket | | +| dependency | Abhängigkeit | | +| variable | Variable | | +| parameter | Parameter | | +| magic command | Magic Command | Untranslated, as Jupyter users say it | +| meta-command | Meta-Befehl | | +| result set | Ergebnismenge | | +| schema | Schema | | +| member (of a type) | Member | Singular and plural alike, as Microsoft's German docs have it | +| renderer, handler | Renderer, Handler | Kept, as German developers say them | +| serializer | Serialisierer | | +| formatter | Formatierer | | + +One departure from a note: `Serve_PressCtrlC` says `Ctrl+C` is the same in every language, but a +German keyboard prints **Strg**, so the German reads `Strg+C`. Key names follow the keyboard the +reader has, which is the rule the `Key_*` entries already state. + +### Spanish + +| English | Spanish | Why | +|---|---|---| +| notebook | cuaderno | What the editor itself says in Spanish, so a reader meets the same word twice | +| cell | celda | | +| output | salida | | +| kernel | kernel | Plural is kernels | +| extension | extensión | | +| panel | panel | | +| theme | tema | | +| layout | diseño | The editor's own word. It does not collide with **tema**, so both stay plain | +| dashboard | Dashboard | Kept. The obvious translation is *panel*, which already means the docked region | +| baseline | línea base | | +| package | paquete | | +| dependency | dependencia | | +| variable | variable | | +| parameter | parámetro | | +| magic command | comando mágico | | +| meta-command | metacomando | | +| result set | conjunto de resultados | | +| schema | esquema | | +| member (of a type) | miembro | | +| handler | controlador | Microsoft's Spanish term, which is what the editor uses | +| renderer, render | renderizador, renderizar | What Spanish developers say, over Microsoft's *representador* | +| serializer | serializador | | +| formatter | formateador | | +| commit (version control) | confirmación | | +| required | obligatorio | | +| default | predeterminado | | + +Two collisions worth knowing about, because the English words are distinct and the obvious +Spanish is not. `Table_Kind` (light or dark) is **Clase** so that `Table_Type` can stay **Tipo**, +and `Table_DisplayName` is **Nombre visible** so that `Table_Name` can stay **Nombre**. + +Where a count and an adjective have to agree, the count is moved rather than guessed at. The npm +audit severities read `altas: {0}` rather than `{0} altas`, because `{0}` is 1 as often as not and +Spanish would need `alta` there. + +### Japanese + +| English | Japanese | Why | +|---|---|---| +| notebook | ノートブック | What the editor itself says in Japanese | +| cell | セル | | +| output | 出力 | | +| kernel | カーネル | | +| extension | 拡張機能 | Matches the editor's own Japanese | +| panel | パネル | | +| theme | テーマ | | +| layout | レイアウト | | +| dashboard | ダッシュボード | No collision with **パネル**, so both stay plain | +| baseline | ベースライン | | +| package | パッケージ | | +| dependency | 依存関係 | | +| variable | 変数 | | +| parameter | パラメーター | With the long vowel mark, as Microsoft's Japanese has it | +| magic command | マジックコマンド | | +| meta-command | メタコマンド | | +| result set | 結果セット | | +| schema | スキーマ | | +| member (of a type) | メンバー | | +| renderer, handler, formatter, serializer | レンダラー、ハンドラー、フォーマッター、シリアライザー | Transliterated, as Japanese developers say them | +| commit (version control) | コミット | | +| required | 必須 | | +| default | 既定 | Microsoft's Japanese, over デフォルト | +| run / execute | 実行 | One word for both, as the source asks | + +Japanese has one form where English has two, so every `_One` and `_Other` pair is translated +identically. That is expected here and not a copy-and-paste slip. + +Counts read as **{0} 件** or **{0} 個** rather than being placed like an English adjective. The +npm audit severities are **高 {0} 件**, not **{0} 高**, because Japanese puts the counter after the +number and the classifier before it. + +Two headings need distinct words the obvious translation would collapse. `Table_Kind` (light or +dark) is **種類** so that `Table_Type` can stay **型**, and `Magic_Schema_ColumnNullable` is +**NULL 可** rather than a phrase, because the column is narrow and NULL is written the same in +every language. + +One string is worth a reviewer's eye: `configuration.showOpenInVersoMenu.description` quotes the +editor's own **Reopen Editor With...** command, rendered here as 「エディターを再度開く...」. If +VS Code's Japanese words that command differently, match the editor rather than this file. + +### Simplified Chinese + +| English | Simplified Chinese | Why | +|---|---|---| +| notebook | 笔记本 | What the editor itself says in Chinese | +| cell | 单元格 | Matches the editor's own Chinese | +| output | 输出 | | +| kernel | 内核 | Matches the editor's own Chinese | +| extension | 扩展 | Matches the editor's own Chinese | +| panel | 面板 | | +| theme | 主题 | | +| layout | 布局 | | +| dashboard | 仪表板 | No collision with **面板**, so both stay plain | +| baseline | 基线 | | +| package | 包 | | +| dependency | 依赖项 | | +| variable | 变量 | | +| parameter | 参数 | | +| magic command | 魔法命令 | | +| meta-command | 元命令 | | +| result set | 结果集 | | +| schema (of a database) | 架构 | Microsoft's Chinese for the database sense, not 模式 | +| repository (version control) | 存储库 | Microsoft's Chinese, which the editor also uses | +| tag (version control) | 标记 | | +| tag (on a cell) | 标签 | Deliberately not 标记, so a cell tag never reads as a git tag | +| member (of a type) | 成员 | | +| renderer, render | 渲染器、渲染 | What Chinese developers say, over Microsoft's 呈现 | +| handler | 处理程序 | | +| provider | 提供程序 | | +| formatter | 格式化程序 | | +| serializer | 序列化程序 | | +| commit (version control) | 提交 | | +| required | 必需 | | +| default | 默认 | | +| run / execute | 运行 | One word for both, as the source asks | +| widget | 小组件 | | + +Chinese has one form where English has two, so every `_One` and `_Other` pair is translated +identically. That is expected here and not a copy-and-paste slip. + +Counts read as **{0} 个** or **{0} 项** rather than being placed like an English adjective, and a +count that heads a clause moves behind its verb: `Compare_SummaryAdded` is **已添加 {0} 个**, not +**{0} 已添加**. The npm audit severities are **高 {0} 个**, because Chinese puts the measure word +after the number and the severity before it. + +Three headings need distinct words the obvious translation would collapse. `Table_Kind` (light or +dark) is **种类** so that `Table_Type` can stay **类型**; `Table_Extensions` (the file extensions a +format is read from) is **扩展名** so that the add-ons can stay **扩展**; and `Common_Dismiss` is +**忽略** so that `Common_Close` can stay **关闭**, since both would otherwise be 关闭. + +Punctuation follows Chinese convention: full-width **,。:()** with no space before them, and +**、** between items in a list. The three-dot ellipsis of the source is kept as typed, because +that is what the editor's own Chinese does. + +One string is worth a reviewer's eye, the same one Japanese flagged: +`configuration.showOpenInVersoMenu.description` quotes the editor's own **Reopen Editor With...** +command, rendered here as “重新打开文件方式...”. If VS Code's Chinese words that command +differently, match the editor rather than this file. + +## Tone and shape + +- Address the reader the way the target language's own software does. German uses **Sie**; + Japanese uses **です・ます**. +- Match the source's register: buttons and menu entries are short and imperative, messages + are complete sentences with a full stop. +- Keep it about as long as the English. A button label that doubles in length is clipped. +- Reproduce the source's punctuation and capitalisation conventions for the target + language, not for English. German capitalises nouns; Japanese and Chinese use their own + full stop and need no space before it. +- Never add quotation marks, brackets, or a trailing full stop the English does not have. +- Translate nothing that is already a code sample or a literal value. diff --git a/build/i18n/merge.py b/build/i18n/merge.py new file mode 100644 index 00000000..121aea58 --- /dev/null +++ b/build/i18n/merge.py @@ -0,0 +1,115 @@ +"""Puts translated strings back into the files the build reads. + +Takes what `export.py` asked for and somebody answered, and writes each string into the +resource file for its language. Translations already there are kept, so a language can be +done in passes, and a key English no longer has is dropped rather than carried in four +languages under a name nothing looks up. + + python3 build/i18n/merge.py build/i18n/pending/de.answer.json + +The answer names its own language, so there is no argument to get wrong. Its shape is the +export's, with each string in place of the object describing it: + + { + "locale": "de", + "sets": { + "Verso.Blazor.Shared/UI": { "Toolbar_Run": "Ausführen" } + } + } + +A translation that dropped a placeholder, or came back empty, or answers a key English does +not have, is refused and named. Writing it would leave a message with a hole in it that +nothing notices until it is finally shown. Everything sound in the same run is still +written, so a rerun only has to cover what was named. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +from resources import LANGUAGE_NAMES, LOCALES, Entry, discover, display, placeholders + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("answer", type=Path, help="The filled-in file to read.") + args = parser.parse_args() + + if not args.answer.exists(): + print(f"{args.answer} does not exist.", file=sys.stderr) + return 1 + + payload = json.loads(args.answer.read_text(encoding="utf-8")) + locale = payload.get("locale") + if locale not in LOCALES: + print(f"The file names its language as {locale!r}, which Verso does not ship.", file=sys.stderr) + return 1 + + by_name = {resource_set.name: resource_set for resource_set in discover()} + refused: list[str] = [] + written = 0 + + for name, answers in payload.get("sets", {}).items(): + resource_set = by_name.get(name) + if resource_set is None: + refused.append(f"{name}: no such resource set") + continue + + source = resource_set.source() + existing = resource_set.translation(locale) + accepted: dict[str, str] = {} + + for key, value in answers.items(): + if not isinstance(value, str): + refused.append(f"{name}/{key}: expected a translation, found {type(value).__name__}") + elif key not in source: + refused.append(f"{name}/{key}: not a key English has") + elif not value.strip(): + refused.append(f"{name}/{key}: empty") + elif sorted(placeholders(source[key].value)) != sorted(placeholders(value)): + refused.append( + f"{name}/{key}: placeholders differ, English has " + f"{sorted(placeholders(source[key].value)) or 'none'} and this has " + f"{sorted(placeholders(value)) or 'none'}" + ) + else: + accepted[key] = value + + if not accepted: + continue + + # Rebuilt from the English keys rather than updated in place, so a key that was + # renamed or dropped since the last pass leaves with it. Notes stay in the neutral + # file: they are written for a translator, and nothing reads them back out of here. + merged = { + key: Entry(accepted.get(key) or existing[key].value) + for key in source + if key in accepted or key in existing + } + + path = resource_set.path_for(locale) + path.parent.mkdir(parents=True, exist_ok=True) + resource_set.save(path, merged) + + outstanding = len(source) - len(merged) + note = f", {outstanding} still missing" if outstanding else "" + print(f" {display(path)}: {len(accepted)} written, {len(merged)} of {len(source)}{note}") + written += len(accepted) + + plural = "string" if written == 1 else "strings" + print(f"\n{written} {plural} merged into {LANGUAGE_NAMES[locale]}.", flush=True) + + if refused: + print(f"\n{len(refused)} refused:", file=sys.stderr) + for problem in refused: + print(f" {problem}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/build/i18n/pseudo.py b/build/i18n/pseudo.py new file mode 100644 index 00000000..a2899019 --- /dev/null +++ b/build/i18n/pseudo.py @@ -0,0 +1,74 @@ +"""Generates the pseudo-locale from the English strings. + +The pseudo-locale is a translation nobody reads. Every letter is accented and every string +is bracketed and padded, which makes it worth running for two reasons: anything still +showing plain English is a string somebody forgot to move into a resource file, and +anything clipped or wrapped badly is a place where a real translation, which tends to run +longer than English, will not fit. + + python3 build/i18n/pseudo.py + +Rerun it whenever English strings are added or changed. The output is committed, so a +reviewer sees the same thing the next person to run the interface will. +""" + +from __future__ import annotations + +import sys + +from resources import PLACEHOLDER, PSEUDO, Entry, discover, display + +# One accented stand-in per letter, chosen to stay recognisable so a screenshot is still +# readable enough to tell which string is which. +ACCENTS = str.maketrans( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + "àbçdéfghïjklmñòpqrštùvwxÿzÀBÇDÉFGHÏJKLMÑÒPQRŠTÙVWXÝZ", +) + +# Padding, so a string that only just fits in English is seen not to fit here. Real +# translations into German run roughly a third longer than the English they come from. +PAD = "···" + + +def disguise(text: str) -> str: + """Accents a string, leaving its placeholders alone. + + A placeholder is filled in at runtime with a name or a number, so accenting one would + either break the lookup or produce a value the reader cannot recognise. + """ + parts: list[str] = [] + position = 0 + + for match in PLACEHOLDER.finditer(text): + parts.append(text[position : match.start()].translate(ACCENTS)) + parts.append(match.group(0)) + position = match.end() + + parts.append(text[position:].translate(ACCENTS)) + return "".join(parts) + + +def main() -> int: + sets = discover() + if not sets: + print("No neutral resource files found.", file=sys.stderr) + return 1 + + for resource_set in sets: + source = resource_set.source() + generated = { + key: Entry(f"[!!{disguise(entry.value)}{PAD}!!]") + for key, entry in source.items() + } + + path = resource_set.path_for(PSEUDO) + path.parent.mkdir(parents=True, exist_ok=True) + resource_set.save(path, generated) + + print(f"{len(generated):5d} {display(path)}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/build/i18n/resources.py b/build/i18n/resources.py new file mode 100644 index 00000000..1ca13030 --- /dev/null +++ b/build/i18n/resources.py @@ -0,0 +1,281 @@ +"""Reading and writing the files that hold Verso's translatable strings. + +Two formats carry the interface: .NET resource files under `src/**/Resources`, and the +JSON bundles the editor extension uses. They differ enough in shape that the scripts +around them would each grow two code paths, so both are wrapped here and everything else +in this directory works in terms of keys, values, and translator notes. + +Run nothing here directly. `export.py`, `merge.py`, `translate.py`, `pseudo.py`, and +`check.py` are the entry points. +""" + +from __future__ import annotations + +import json +import re +import xml.etree.ElementTree as ElementTree +from dataclasses import dataclass +from pathlib import Path +from xml.sax.saxutils import escape + +# Where the scripts sit relative to the repository. +REPO_ROOT = Path(__file__).resolve().parents[2] + +# The languages Verso ships an interface in, English aside. Mirrors VersoCultures.Supported. +LOCALES = ["de", "es", "ja", "zh-Hans"] + +# Generated rather than translated, and never offered in a picker. See pseudo.py. +PSEUDO = "qps-Ploc" + +# Human names, used to address the translator and to caption a report. +LANGUAGE_NAMES = { + "de": "German", + "es": "Spanish", + "ja": "Japanese", + "zh-Hans": "Simplified Chinese", + PSEUDO: "Pseudo-locale", +} + +# The editor names languages its own way, so its file names do not match the .NET ones. +# Only the entries that actually differ are listed; anything absent is used as written. +VSCODE_IDS = { + "zh-Hans": "zh-cn", + PSEUDO: "qps-ploc", +} + +# Anything a translation has to carry through unchanged: .NET's positional {0} and the +# named {placeholder} the editor bundles use. Doubled braces are an escaped literal brace +# and are matched first so they are not mistaken for an empty placeholder. +PLACEHOLDER = re.compile(r"\{\{|\}\}|\{[^{}]*\}") + +# Copied from a resource file written by the .NET tooling, so generated files are +# byte-identical in everything but their entries and no editor reformats them on open. +RESX_HEADER = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + +""" + + +@dataclass +class Entry: + """One string, and whatever context a translator was given for it.""" + + value: str + comment: str = "" + + +def display(path: Path) -> str: + """A path as it should appear in a report, relative to the repository where it can be.""" + try: + return str(path.relative_to(REPO_ROOT)) + except ValueError: + return str(path) + + +def placeholders(text: str) -> list[str]: + """The placeholder tokens in a string, in the order they appear. + + Which tokens are present is what a check compares, not the order they arrive in: a + translation is free to move `{0}` behind `{1}` because the target language puts the + sentence together differently, but a translation that loses `{0}` altogether produces + a message with a hole in it rather than an error. + """ + return [m.group(0) for m in PLACEHOLDER.finditer(text) if m.group(0) not in ("{{", "}}")] + + +class ResourceSet: + """A neutral file and the translated files that shadow it.""" + + def __init__(self, neutral: Path): + self.neutral = neutral + + @property + def name(self) -> str: + """A short, stable way to name this set in a report or a handoff file. + + The full path is unwieldy to type and to read, and the file name alone is + ambiguous: ten assemblies each have a `Strings.resx`. So a set is named by what + distinguishes it, which is the assembly it belongs to. + """ + raise NotImplementedError + + def path_for(self, locale: str) -> Path: + raise NotImplementedError + + def load(self, path: Path) -> dict[str, Entry]: + raise NotImplementedError + + def save(self, path: Path, entries: dict[str, Entry]) -> None: + raise NotImplementedError + + def source(self) -> dict[str, Entry]: + return self.load(self.neutral) + + def translation(self, locale: str) -> dict[str, Entry]: + """The entries already translated into a language, empty when the file is new.""" + path = self.path_for(locale) + return self.load(path) if path.exists() else {} + + +class ResxSet(ResourceSet): + """A .NET resource file, which compiles into one satellite assembly per language.""" + + @property + def name(self) -> str: + # `src/Verso.Ado/Resources/Strings.resx` is named `Verso.Ado/Strings`, which is the + # assembly whose satellite it becomes and the file within it. + return f"{self.neutral.parents[1].name}/{self.neutral.stem}" + + def path_for(self, locale: str) -> Path: + return self.neutral.with_name(f"{self.neutral.stem}.{locale}.resx") + + def load(self, path: Path) -> dict[str, Entry]: + root = ElementTree.parse(path).getroot() + entries: dict[str, Entry] = {} + + for data in root.findall("data"): + name = data.get("name") + # Entries carrying a type or mimetype hold something other than a string, + # such as an icon. There are none today, and translating one would be wrong. + if name is None or data.get("type") or data.get("mimetype"): + continue + + value = data.findtext("value") or "" + comment = data.findtext("comment") or "" + entries[name] = Entry(value, comment) + + return entries + + def save(self, path: Path, entries: dict[str, Entry]) -> None: + lines = [RESX_HEADER] + + for name in sorted(entries): + entry = entries[name] + lines.append(f' \n') + lines.append(f" {escape(entry.value)}\n") + if entry.comment: + lines.append(f" {escape(entry.comment)}\n") + lines.append(" \n") + + lines.append("") + path.write_text("".join(lines), encoding="utf-8") + + +class JsonSet(ResourceSet): + """One of the editor extension's bundles. + + Covers both `package.nls.json`, which names commands and settings, and + `bundle.l10n.json`, which holds the strings the extension code passes through + `vscode.l10n.t`. They share a format: a key maps either to the string itself or to an + object carrying the string plus notes for whoever translates it. + """ + + @property + def name(self) -> str: + # Already unique and already short, so the path is the name, less the extension. + return display(self.neutral)[: -len(".json")] + + def path_for(self, locale: str) -> Path: + stem = self.neutral.name[: -len(".json")] + return self.neutral.with_name(f"{stem}.{VSCODE_IDS.get(locale, locale)}.json") + + def load(self, path: Path) -> dict[str, Entry]: + raw = json.loads(path.read_text(encoding="utf-8")) + entries: dict[str, Entry] = {} + + for name, value in raw.items(): + if isinstance(value, dict): + comment = value.get("comment", "") + if isinstance(comment, list): + comment = " ".join(comment) + entries[name] = Entry(value.get("message", ""), comment) + else: + entries[name] = Entry(value) + + return entries + + def save(self, path: Path, entries: dict[str, Entry]) -> None: + # Notes are for translators, so they stay in the neutral file and are not copied + # into the translations, where they would only be read back by the next script. + body = {name: entries[name].value for name in sorted(entries)} + path.write_text( + json.dumps(body, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + + +def discover() -> list[ResourceSet]: + """Every neutral file in the repository, in a stable order. + + A file is neutral when its name carries no language, so `UI.resx` is a source and + `UI.de.resx` is one of its translations. + """ + sets: list[ResourceSet] = [] + + for path in sorted((REPO_ROOT / "src").glob("**/Resources/*.resx")): + if "." in path.stem or {"bin", "obj"} & set(path.parts): + continue + sets.append(ResxSet(path)) + + for relative in ("vscode/package.nls.json", "vscode/l10n/bundle.l10n.json"): + path = REPO_ROOT / relative + if path.exists(): + sets.append(JsonSet(path)) + + return sets diff --git a/build/i18n/translate.py b/build/i18n/translate.py new file mode 100644 index 00000000..854f65e0 --- /dev/null +++ b/build/i18n/translate.py @@ -0,0 +1,224 @@ +"""Fills in the translations that are missing from the shipped languages, against the API. + +Reads every neutral resource file, works out which keys a language has not been given yet, +and asks Claude for those and only those. Existing translations are left alone, so adding a +handful of English strings costs a handful of translations rather than a retranslation of +the interface. + +This is one of two routes, and the one that costs an API key. `export.py` and `merge.py` +are the other: they do the same reading and the same writing, and hand the part in between +to whoever is translating. See `README.md`. + + pip install anthropic + export ANTHROPIC_API_KEY=... + python3 build/i18n/translate.py # everything missing, all languages + python3 build/i18n/translate.py --locale ja # one language + python3 build/i18n/translate.py --all # replace what is already there + python3 build/i18n/translate.py --dry-run # report the work without doing it + +The output is committed, and nothing in the build or in continuous integration runs this, +so no API key is needed to build Verso or to check the translations. Run `check.py` +afterwards, and have a reader of the language look over the result before shipping it. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys + +from resources import ( + LANGUAGE_NAMES, + LOCALES, + REPO_ROOT, + Entry, + discover, + display, + placeholders, +) + +MODEL = "claude-opus-5" + +# Enough for a batch of short interface strings with room to spare. A batch that would +# exceed it fails loudly rather than returning a truncated object, because the response has +# to parse as JSON to be used at all. +MAX_TOKENS = 16000 + +# Small enough that one bad batch is cheap to redo, large enough that the model sees +# neighbouring strings and keeps their wording consistent with each other. +BATCH_SIZE = 40 + +SYSTEM = """You are translating the interface of Verso, a computational notebook \ +application, from English into {language}. The people reading it are programmers, data \ +analysts, and scientists, and they will read your translation while working. + +Follow this glossary exactly. + +{glossary} + +You are given a JSON array. Each element has a `key` naming the string, a `source` holding \ +the English, and sometimes a `note` written for you by the developer explaining where the \ +string appears and what constrains it. Honour the note. + +Reply with a JSON object mapping each key to its translation, and nothing else: no prose \ +before or after it, no code fence, no commentary. Include every key you were given.""" + + +def build_prompt(items: list[tuple[str, Entry]]) -> str: + payload = [] + for key, entry in items: + element = {"key": key, "source": entry.value} + if entry.comment: + element["note"] = entry.comment + payload.append(element) + + return json.dumps(payload, ensure_ascii=False, indent=2) + + +def parse_reply(text: str) -> dict[str, str]: + """Reads the model's reply, tolerating a code fence it was asked not to add.""" + body = text.strip() + + if body.startswith("```"): + body = body.split("\n", 1)[1] if "\n" in body else "" + if body.rstrip().endswith("```"): + body = body.rstrip()[: -len("```")] + + return json.loads(body) + + +def translate_batch(client, locale: str, glossary: str, items: list[tuple[str, Entry]]) -> dict[str, str]: + """One request. Returns the translations that came back, whether or not they are sound.""" + system = SYSTEM.format(language=LANGUAGE_NAMES[locale], glossary=glossary) + + # Streamed because a large batch can run long enough to reach the request timeout, and + # a timeout here would throw away a batch that was nearly finished. + with client.messages.stream( + model=MODEL, + max_tokens=MAX_TOKENS, + thinking={"type": "adaptive"}, + system=system, + messages=[{"role": "user", "content": build_prompt(items)}], + ) as stream: + message = stream.get_final_message() + + text = "".join(block.text for block in message.content if block.type == "text") + return parse_reply(text) + + +def sound(source: str, translation: str) -> bool: + """Whether a translation can be used without a person looking at it first.""" + return bool(translation.strip()) and sorted(placeholders(source)) == sorted( + placeholders(translation) + ) + + +def translate(client, locale: str, glossary: str, items: list[tuple[str, Entry]]) -> dict[str, str]: + """Translates a batch and retries once for anything that came back unusable.""" + result = translate_batch(client, locale, glossary, items) + + retry = [ + (key, entry) + for key, entry in items + if not sound(entry.value, result.get(key, "")) + ] + + if retry: + print(f" retrying {len(retry)} strings", flush=True) + result.update(translate_batch(client, locale, glossary, retry)) + + for key, entry in items: + if not sound(entry.value, result.get(key, "")): + # Left to English rather than written out wrong. check.py reports it as + # missing, which is the truth and is fixable by rerunning. + print(f" gave up on {key}", file=sys.stderr) + result.pop(key, None) + + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "--locale", + action="append", + choices=LOCALES, + help="Translate one language. Repeatable. Defaults to all of them.", + ) + parser.add_argument( + "--all", + action="store_true", + help="Retranslate strings that already have a translation.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Report what would be translated without calling the API.", + ) + args = parser.parse_args() + + locales = args.locale or LOCALES + glossary = (REPO_ROOT / "build" / "i18n" / "glossary.md").read_text(encoding="utf-8") + + sets = discover() + if not sets: + print("No neutral resource files found.", file=sys.stderr) + return 1 + + client = None + if not args.dry_run: + try: + import anthropic + except ImportError: + print("This needs the anthropic package: pip install anthropic", file=sys.stderr) + return 1 + + if not os.environ.get("ANTHROPIC_API_KEY"): + print("Set ANTHROPIC_API_KEY.", file=sys.stderr) + return 1 + + client = anthropic.Anthropic() + + for resource_set in sets: + source = resource_set.source() + + for locale in locales: + existing = resource_set.translation(locale) + wanted = [ + (key, source[key]) + for key in sorted(source) + if args.all or key not in existing + ] + + if not wanted: + continue + + path = resource_set.path_for(locale) + print(f"{display(path)}: {len(wanted)} strings", flush=True) + + if args.dry_run: + continue + + translations: dict[str, str] = {} + for start in range(0, len(wanted), BATCH_SIZE): + batch = wanted[start : start + BATCH_SIZE] + print(f" {start + 1}-{start + len(batch)}", flush=True) + translations.update(translate(client, locale, glossary, batch)) + + # Rebuilt from the English keys, so a key that was renamed or dropped leaves + # with it rather than lingering in four languages. + merged = { + key: Entry(translations.get(key) or existing[key].value) + for key in source + if key in translations or key in existing + } + + path.parent.mkdir(parents=True, exist_ok=True) + resource_set.save(path, merged) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/guides/cli-reference.md b/docs/guides/cli-reference.md index a03e0bb2..72934a95 100644 --- a/docs/guides/cli-reference.md +++ b/docs/guides/cli-reference.md @@ -16,6 +16,14 @@ dotnet tool update -g Verso.Cli It requires the .NET 8.0 SDK or later. After installing, `verso --help` lists the commands, and every command has its own `--help`. +## Global options + +| Option | Default | Description | +|--------|---------|-------------| +| `--language ` | system | Interface language: `en`, `de`, `es`, `ja`, or `zh-Hans` | + +`--language` is accepted by every command and before the command name as well, so `verso --language de --help` prints the help in German. With no option the language comes from the `VERSO_LANGUAGE` environment variable, then from the operating system, then English. Only the words change: numbers and dates are written the way the machine writes them, and the `[error]` tags and `--output json` status values stay in English so a pipeline that reads them cannot break. See [Interface Language](interface-language.md). + ## verso serve Launches the editor as a local web app and opens it in your browser. diff --git a/docs/guides/interface-language.md b/docs/guides/interface-language.md new file mode 100644 index 00000000..be79cb2e --- /dev/null +++ b/docs/guides/interface-language.md @@ -0,0 +1,111 @@ +# Interface Language + +Verso's interface is written in English and translated into German, Spanish, Japanese, and Simplified Chinese. The translation covers what Verso itself draws: the notebook interface, the toolbar and panels, the messages a kernel puts in a cell's output, and the command-line tool. What your notebook prints is your own, and Verso never rewrites it. + +| Tag | Language | +|-----|----------| +| `en` | English | +| `de` | Deutsch | +| `es` | Español | +| `ja` | 日本語 | +| `zh-Hans` | 简体中文 | + +Translations ship inside Verso, so nothing is downloaded and no account or key is involved. + +## Choosing a language + +Every host asks the same question in its own way and answers it the same way. The first of these that names a language Verso has, wins: + +| Order | Source | Where it comes from | +|-------|--------|---------------------| +| 1 | Explicit request | `--language` on the command line, or `verso.language` in VS Code | +| 2 | Environment override | The `VERSO_LANGUAGE` environment variable | +| 3 | Operating system | The system's own interface language | +| 4 | English | The language every string is written in | + +A tag that names a region falls back to the language: `de-AT` finds German, and `zh-CN` finds Simplified Chinese. A tag Verso does not have falls through to the next source rather than failing, so a misspelling costs you the translation and nothing else. + +## In VS Code + +`verso.language` sets the language of the notebook interface and of the kernel messages that appear in cell output. It defaults to `Auto-detect`, which follows the editor's own display language. + +```jsonc +"verso.language": "ja" +``` + +There is a limit worth knowing before you go looking for the setting that fixes it. Entries in menus, command names in the Command Palette, and the descriptions of these settings all come from the editor, which draws them in its display language and gives no extension a way to override it. So a workbench set to English and `verso.language` set to Japanese will show **Compare** in the Command Palette and the Compare panel inside the notebook in Japanese, at the same time. That is the intended behaviour and not a missed string. + +A change applies the next time a notebook is opened. The interface is a WebAssembly application that takes its language when it starts, and the host process behind it is launched with the language on its command line, so neither can be re-languaged without starting again. Reloading whatever is already open would throw away unsaved work to change a menu. + +## From the terminal + +`--language` is accepted by every command, and before the command name as well, so help text comes out in the language you asked for: + +```bash +verso --language de --help +verso run pipeline.verso --language ja +verso repl --language es +verso serve --language zh-Hans +``` + +The command-line library Verso is built on writes its own usage headings and its own parse errors, and does not offer them for translation, so `Usage:`, `Options:`, and a message about a missing required argument stay in English whatever you ask for. + +## In a container or a pipeline + +`VERSO_LANGUAGE` sets the language once for everything Verso runs, which is easier than adding an option to every invocation: + +```bash +export VERSO_LANGUAGE=de +verso run pipeline.verso +``` + +An explicit `--language` still wins over it, so a single run can differ without the variable being changed. + +```dockerfile +ENV VERSO_LANGUAGE=ja +``` + +If you parse Verso's output in a pipeline, the parts you would parse are not translated. The `[stderr]` and `[error]` tags a run writes, and the status values in the document `--output json` produces, stay in English exactly so that setting a language cannot break a script. + +## Serving to a browser + +`verso serve` with no `--language` lets each browser ask, through the `Accept-Language` header it already sends, so two people opening the same server can read it in two languages. `verso serve --language de` pins it instead, and every browser gets German. + +```bash +verso serve # each browser is asked +verso serve --language de # everybody gets German +``` + +## Numbers and dates + +Choosing a language translates words. It does not change how numbers, dates, and currency are written, because that would change your results rather than translate the interface: the same cell would print `3.14` on one machine and `3,14` on another, and that difference would be saved into the notebook file. + +The one place both move together is the VS Code notebook interface, where the browser runtime takes a single culture for both and offers no way to split them. Cells there still run in the host process, which keeps its own formatting, so what a cell prints is unaffected. + +## What stays in English + +Four kinds of text are deliberately never translated, and each is marked as such in the source: + +- **Anything a model reads rather than a person.** The chat participant's instructions and the descriptions of the tools it can call. Translating them changes which tool gets chosen without changing anything you see. +- **Anything a script reads rather than a person.** The output tags and JSON status values described above. +- **Text that only a fault produces.** Log lines and guards against programmer error stay searchable, so a stack trace pasted into an issue still matches a search made in another language. +- **The shape of the protocol.** The editor and the host talk over a small JSON-RPC surface, and a malformed request is a fault in code rather than something a reader can act on. + +The line between the last two and everything else is whether you can do something about the message. A package that would not download, a value that does not fit the type its parameter declares, a connection that has since closed: all translated. A cell id that does not exist: not. + +## Adding a language + +Translations are ordinary files in the repository, so a new language is a pull request rather than a release. The `build/i18n` directory holds the tooling and a README covering the whole route: add the tag to the shipped list in four places, export the strings a language is missing, translate them by hand or against an API, and merge them back. The merge refuses a translation that dropped a placeholder, came back empty, or names a string English does not have. + +Two development aids are worth knowing about if you work on this. `check.py` compares every language against the English and reports anything that has drifted; it runs in continuous integration and needs no key and no network. And `qps-Ploc` is a generated pseudo-language in which every translated string appears accented and bracketed, so running the interface in it shows at a glance which strings were never moved into a resource file and where a longer translation would be clipped: + +```bash +verso serve --language qps-Ploc +``` + +It is deliberately absent from the `verso.language` dropdown, because it is a development aid rather than a language. Set it by hand to use it in the editor. + +## See also + +- [CLI Reference](cli-reference.md) +- [Getting Started](getting-started.md) diff --git a/src/Verso.Abstractions/Localization/Plural.cs b/src/Verso.Abstractions/Localization/Plural.cs new file mode 100644 index 00000000..f1d83ab7 --- /dev/null +++ b/src/Verso.Abstractions/Localization/Plural.cs @@ -0,0 +1,29 @@ +namespace Verso.Abstractions; + +/// +/// Chooses between the two forms of a message that counts something. +/// +/// +/// Two forms cover every language Verso ships in: German and Spanish need a singular and a +/// plural, and Japanese and Chinese need neither. A language with more forms, such as Russian +/// or Polish, does not fit here and would need a real plural selector keyed on the count and +/// the language together. Adding one of those means replacing this, not adding a third key. +/// +/// The alternative, building a word out of a stem and an s, does not survive translation +/// at all: the plural of a German noun is not its singular with a letter on the end. Neither +/// does cell(s), which no other language can copy. +/// +/// +/// It lives here, in the assembly every other one already references, because the notebook +/// interface, the engine, the command line, and each kernel all count something. An extension +/// writing its own messages can use it for the same reason. +/// +/// +public static class Plural +{ + /// Picks the singular for exactly one, and the plural for anything else. + /// How many things the message is about. + /// The message written for a single thing. + /// The message written for any other number of them. + public static string Of(int count, string one, string other) => count == 1 ? one : other; +} diff --git a/src/Verso.Abstractions/Theming/ThemeCss.cs b/src/Verso.Abstractions/Theming/ThemeCss.cs new file mode 100644 index 00000000..80871849 --- /dev/null +++ b/src/Verso.Abstractions/Theming/ThemeCss.cs @@ -0,0 +1,143 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Text; + +namespace Verso.Abstractions; + +/// +/// Builds the :root { --verso-*: ...; } custom property block that Verso surfaces +/// style themselves from. +/// +/// +/// +/// There is one emitter rather than one per surface. The notebook interface and the +/// self-contained HTML export need the same block, and while each kept its own copy the two +/// drifted: the export never emitted the typography tokens that are not fonts, so +/// --verso-font-family-mono, --verso-font-family-sans and +/// --verso-font-size-base were missing from every exported document. +/// +/// +/// Every number is written with the invariant culture. A stylesheet is not read in anyone's +/// language: a length is 1.4 whoever is looking at it, never 1,4, and a browser +/// discards a declaration it cannot parse. Callers cannot get this wrong by accident because +/// they never format a number themselves. +/// +/// +public static class ThemeCss +{ + /// + /// Builds the custom property block for a theme, falling back to the default tokens for + /// anything the theme does not supply. + /// + public static string BuildRootBlock(ITheme? theme) => + BuildRootBlock(theme?.Colors, theme?.Typography, theme?.Spacing, theme?.Elevation); + + /// + /// Builds the custom property block from individual token groups, falling back to the + /// defaults for any group that is null. + /// + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(ThemeColorTokens))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(ThemeTypography))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(ThemeSpacing))] + [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(ThemeElevation))] + public static string BuildRootBlock( + ThemeColorTokens? colors, + ThemeTypography? typography, + ThemeSpacing? spacing, + ThemeElevation? elevation) + { + var sb = new StringBuilder(); + sb.AppendLine(":root {"); + AppendColors(sb, colors ?? new ThemeColorTokens()); + AppendTypography(sb, typography ?? new ThemeTypography()); + AppendSpacing(sb, spacing ?? new ThemeSpacing()); + AppendElevation(sb, elevation ?? new ThemeElevation()); + sb.AppendLine("}"); + return sb.ToString(); + } + + private static void AppendColors(StringBuilder sb, ThemeColorTokens colors) + { + foreach (var prop in typeof(ThemeColorTokens).GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (prop.PropertyType != typeof(string)) continue; + var value = (string?)prop.GetValue(colors) ?? ""; + sb.AppendLine($" --verso-{ToKebabCase(prop.Name)}: {value};"); + } + } + + private static void AppendTypography(StringBuilder sb, ThemeTypography typography) + { + foreach (var prop in typeof(ThemeTypography).GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + var name = ToKebabCase(prop.Name); + if (prop.PropertyType == typeof(FontDescriptor)) + { + if (prop.GetValue(typography) is not FontDescriptor font) continue; + sb.AppendLine($" --verso-{name}-family: {font.Family};"); + sb.AppendLine(CultureInfo.InvariantCulture, $" --verso-{name}-size: {font.SizePx}px;"); + sb.AppendLine(CultureInfo.InvariantCulture, $" --verso-{name}-weight: {font.Weight};"); + sb.AppendLine(CultureInfo.InvariantCulture, $" --verso-{name}-line-height: {font.LineHeight};"); + } + else if (prop.PropertyType == typeof(string)) + { + if (prop.GetValue(typography) is not string value) continue; + sb.AppendLine($" --verso-{name}: {value};"); + } + else if (prop.PropertyType == typeof(double)) + { + var value = (double)prop.GetValue(typography)!; + sb.AppendLine(CultureInfo.InvariantCulture, $" --verso-{name}: {value}px;"); + } + } + } + + private static void AppendSpacing(StringBuilder sb, ThemeSpacing spacing) + { + foreach (var prop in typeof(ThemeSpacing).GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (prop.PropertyType != typeof(double)) continue; + var value = (double)prop.GetValue(spacing)!; + sb.AppendLine(CultureInfo.InvariantCulture, $" --verso-{ToKebabCase(prop.Name)}: {value}px;"); + } + } + + private static void AppendElevation(StringBuilder sb, ThemeElevation elevation) + { + // Elevation properties are named Level0..Level3. The prefix is dropped so a stylesheet + // reads var(--verso-elevation-1) rather than var(--verso-elevation-level1). + foreach (var prop in typeof(ThemeElevation).GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (prop.PropertyType != typeof(string)) continue; + if (prop.GetValue(elevation) is not string value) continue; + var suffix = ToKebabCase(prop.Name.StartsWith("Level", StringComparison.Ordinal) + ? prop.Name["Level".Length..] + : prop.Name); + sb.AppendLine($" --verso-elevation-{suffix}: {value};"); + } + } + + /// + /// Converts a PascalCase property name to the spelling used in a custom property name, + /// so BgDefault becomes bg-default. + /// + private static string ToKebabCase(string name) + { + var sb = new StringBuilder(); + for (int i = 0; i < name.Length; i++) + { + var c = name[i]; + if (char.IsUpper(c)) + { + if (i > 0) sb.Append('-'); + sb.Append(char.ToLowerInvariant(c)); + } + else + { + sb.Append(c); + } + } + return sb.ToString(); + } +} diff --git a/src/Verso.Ado/CellType/SqlCellRenderer.cs b/src/Verso.Ado/CellType/SqlCellRenderer.cs index 99534915..c9ba1dd4 100644 --- a/src/Verso.Ado/CellType/SqlCellRenderer.cs +++ b/src/Verso.Ado/CellType/SqlCellRenderer.cs @@ -2,6 +2,7 @@ using System.Text; using Verso.Abstractions; using Verso.Ado.MagicCommands; +using Verso.Ado.Resources; namespace Verso.Ado.CellType; @@ -14,10 +15,10 @@ public sealed class SqlCellRenderer : ICellRenderer // --- IExtension --- public string ExtensionId => "verso.ado.renderer.sql"; - public string Name => "SQL Renderer"; + public string Name => Strings.Renderer_Name; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Renders SQL cells with connection indicator badges."; + public string? Description => Strings.Renderer_Description; // --- ICellRenderer --- diff --git a/src/Verso.Ado/CellType/SqlCellType.cs b/src/Verso.Ado/CellType/SqlCellType.cs index 6de04a17..0ad85d05 100644 --- a/src/Verso.Ado/CellType/SqlCellType.cs +++ b/src/Verso.Ado/CellType/SqlCellType.cs @@ -1,5 +1,6 @@ using Verso.Abstractions; using Verso.Ado.Kernel; +using Verso.Ado.Resources; namespace Verso.Ado.CellType; @@ -13,10 +14,10 @@ public sealed class SqlCellType : ICellType // --- IExtension --- public string ExtensionId => "verso.ado.celltype.sql"; - public string Name => "SQL Cell Type"; + public string Name => Strings.CellType_Name; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "SQL cell type for querying databases via ADO.NET."; + public string? Description => Strings.CellType_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; diff --git a/src/Verso.Ado/Formatters/ResultSetFormatter.cs b/src/Verso.Ado/Formatters/ResultSetFormatter.cs index 65faa96f..84c321f4 100644 --- a/src/Verso.Ado/Formatters/ResultSetFormatter.cs +++ b/src/Verso.Ado/Formatters/ResultSetFormatter.cs @@ -4,6 +4,9 @@ using System.Text.Json; using Verso.Abstractions; using Verso.Ado.Models; +using Verso.Ado.Resources; +using System.Text.Json; +using Verso.Abstractions; namespace Verso.Ado.Formatters; @@ -18,10 +21,10 @@ public sealed class ResultSetFormatter : IDataFormatter // --- IExtension --- public string ExtensionId => "verso.ado.formatter.resultset"; - public string Name => "Result Set Formatter"; + public string Name => Strings.Formatter_Name; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Formats SQL result sets and DataTables as paginated HTML tables."; + public string? Description => Strings.Formatter_Description; // --- IDataFormatter --- @@ -106,21 +109,19 @@ internal static string FormatResultSetHtml(SqlResultSet resultSet, IThemeContext } else { - sb.Append("
Showing 1-") - .Append(totalRows.ToString("N0")) - .Append(" of ") - .Append(totalRows.ToString("N0")) - .Append(" rows
"); + sb.Append("
") + .Append(WebUtility.HtmlEncode(string.Format(Strings.Table_ShowingRows, + 1, totalRows.ToString("N0"), totalRows.ToString("N0")))) + .Append("
"); } // Truncation warning if (resultSet.WasTruncated) { - sb.Append("
Results truncated at ") - .Append(resultSet.Rows.Count.ToString("N0")) - .Append(" of ") - .Append(resultSet.TotalRowCount.ToString("N0")) - .Append(" total rows. Use WHERE or LIMIT to narrow your query.
"); + sb.Append("
") + .Append(WebUtility.HtmlEncode(string.Format(Strings.Table_Truncated, + resultSet.Rows.Count.ToString("N0"), resultSet.TotalRowCount.ToString("N0")))) + .Append("
"); } sb.Append(""); @@ -171,10 +172,15 @@ internal static string FormatNonQueryHtml(int rowsAffected, int statementCount, AppendStyles(sb, theme); sb.Append("
"); sb.Append("
"); - sb.Append(rowsAffected.ToString("N0")).Append(" row(s) affected"); + var affected = string.Format( + Plural.Of(rowsAffected, Strings.Table_RowsAffected_One, Strings.Table_RowsAffected_Other), + rowsAffected.ToString("N0")); if (statementCount > 1) - sb.Append(" (").Append(statementCount).Append(" statements)"); - sb.Append(" (").Append(elapsedMs).Append(" ms)"); + affected = string.Format(Strings.Table_Statements, affected, statementCount); + sb.Append(WebUtility.HtmlEncode(affected)); + sb.Append(" (") + .Append(WebUtility.HtmlEncode(string.Format(Strings.Table_Elapsed, elapsedMs))) + .Append(")"); sb.Append("
"); return sb.ToString(); } @@ -187,7 +193,7 @@ internal static string FormatNonQueryHtml(int rowsAffected, int statementCount, /// fallback chain so the same HTML adapts to any host environment: /// /// --vscode-* — VS Code notebook output webview - /// --verso-* — Blazor shell / HTML export (set by ThemeProvider / ThemeCssGenerator) + /// --verso-* — Blazor shell / HTML export (set by ThemeCss.BuildRootBlock) /// literal value — safety net for isolated HTML /// /// The parameter is retained for API compatibility but @@ -242,12 +248,15 @@ internal static void AppendStyles(StringBuilder sb, IThemeContext? theme) private static void AppendPagingScript(StringBuilder sb, int totalRows, int pageSize) { sb.Append("
"); - sb.Append(""); - sb.Append(""); + sb.Append(""); + sb.Append(""); sb.Append(""); sb.Append("
"); sb.Append(" - + + + diff --git a/src/Verso.Blazor/Components/App.razor b/src/Verso.Blazor/Components/App.razor index 2148e7c6..1a482ad9 100644 --- a/src/Verso.Blazor/Components/App.razor +++ b/src/Verso.Blazor/Components/App.razor @@ -1,7 +1,10 @@ @using System.Reflection +@using System.Globalization +@using System.Text.Json +@using Verso.Blazor.Shared.Resources - + @@ -48,9 +51,9 @@ @@ -66,8 +69,8 @@ let disconnectTimer = null; function recoverAndReload() { - title.textContent = 'Recovering your session...'; - message.textContent = 'Reloading to restore your notebook.'; + title.textContent = @Js(UI.Reconnect_RecoveringTitle); + message.textContent = @Js(UI.Reconnect_RecoveringMessage); reloadBtn.style.display = 'none'; overlay.style.display = 'flex'; @@ -100,8 +103,8 @@ if (Date.now() - pageLoadTime < 10000) return; overlay.style.display = 'flex'; - title.textContent = 'Reconnecting...'; - message.textContent = 'Your session is still active. Attempting to reconnect.'; + title.textContent = @Js(UI.Reconnect_Title); + message.textContent = @Js(UI.Reconnect_Message); reloadBtn.style.display = 'none'; // Foreground fallback: if the tab stays visible, reload after 2 min @@ -136,6 +139,11 @@ @code { + // The overlay is written twice: once as markup for the first paint, and once by the + // script that rewrites it when the connection drops. Both read the same resources, so + // the script needs its text as a quoted JavaScript string rather than as page content. + private static MarkupString Js(string value) => new(JsonSerializer.Serialize(value)); + private static readonly string CacheBuster = GetCacheBuster(); private static string GetCacheBuster() diff --git a/src/Verso.Blazor/Components/DiffSourcePickerDialog.razor b/src/Verso.Blazor/Components/DiffSourcePickerDialog.razor index 0965148a..23a38d0e 100644 --- a/src/Verso.Blazor/Components/DiffSourcePickerDialog.razor +++ b/src/Verso.Blazor/Components/DiffSourcePickerDialog.razor @@ -4,7 +4,7 @@ @@ -47,13 +47,11 @@ private bool IsGitRef => Kind == "gitRef"; - private string Title => IsGitRef ? "Compare with Git Ref" : "Compare with File"; + private string Title => IsGitRef ? UI.DiffPicker_TitleGitRef : UI.DiffPicker_TitleFile; - private string Prompt => IsGitRef - ? "Branch, tag, or commit" - : "Path to a notebook file (.verso, .ipynb, .md, .dib)"; + private string Prompt => IsGitRef ? UI.DiffPicker_PromptGitRef : UI.DiffPicker_PromptFile; - private string Placeholder => IsGitRef ? "main" : "/path/to/notebook.verso"; + private string Placeholder => IsGitRef ? UI.DiffPicker_PlaceholderGitRef : UI.DiffPicker_PlaceholderFile; protected override void OnParametersSet() { diff --git a/src/Verso.Blazor/Components/NotebookInputDialog.razor b/src/Verso.Blazor/Components/NotebookInputDialog.razor index 40d390f1..65f4b5b4 100644 --- a/src/Verso.Blazor/Components/NotebookInputDialog.razor +++ b/src/Verso.Blazor/Components/NotebookInputDialog.razor @@ -5,8 +5,8 @@
- Notebook Input -
@@ -24,10 +24,10 @@
@@ -43,7 +43,7 @@ [Parameter] public EventCallback OnCancel { get; set; } private string PromptText => - string.IsNullOrWhiteSpace(Request?.Prompt) ? "Input" : Request.Prompt; + string.IsNullOrWhiteSpace(Request?.Prompt) ? UI.InputDialog_DefaultPrompt : Request.Prompt; private string InputType => Request?.IsPassword == true ? "password" : "text"; diff --git a/src/Verso.Blazor/Components/Pages/NotebookPage.razor b/src/Verso.Blazor/Components/Pages/NotebookPage.razor index b15d2634..a2925e31 100644 --- a/src/Verso.Blazor/Components/Pages/NotebookPage.razor +++ b/src/Verso.Blazor/Components/Pages/NotebookPage.razor @@ -5,7 +5,7 @@ @inject INotebookService Service @inject IJSRuntime JS -Verso Notebook +@UI.Common_AppTitle @@ -39,29 +39,31 @@
- Save Notebook As -
- + + @* One sentence, one resource: which words fall either side of the file + extension is a property of the language, not of this markup. *@
- Enter the full file path. The .verso extension will be added if missing. + @Sentence(UI.SaveDialog_PathHint, ".verso")
@@ -86,7 +88,7 @@ {
@_error - +
} @@ -100,9 +102,9 @@ @if (!Service.IsLoaded) {
-

Verso Notebook

-

Create a new notebook or open an existing .verso, .ipynb, or .md file to get started.

- +

@UI.Common_AppTitle

+

@Sentence(UI.Welcome_Intro, ".verso", ".ipynb", ".md")

+
} else @@ -134,7 +136,7 @@
@ActivePanelTitle - +
@* Host panels are components with direct access to notebook state; @@ -325,7 +327,7 @@ } catch (Exception ex) { - _error = $"Session recovery failed: {ex.Message}"; + _error = string.Format(UI.Error_SessionRecovery, ex.Message); } } @@ -436,7 +438,7 @@ catch (JSException) { } catch (Exception ex) { - _error = $"Session recovery failed: {ex.Message}"; + _error = string.Format(UI.Error_SessionRecovery, ex.Message); try { await JS.InvokeVoidAsync("versoRecovery.clearState"); } catch { } } } @@ -698,9 +700,11 @@ private NotebookPanelInfo? ActivePanelInfo => _activePanel is null ? null : _panels.FirstOrDefault(p => p.Key == _activePanel); + // Drawn in capitals by the stylesheet, not here: which letters have a capital form, + // and what they turn into, depends on the language the header is written in. private string ActivePanelTitle => ActivePanelInfo is { IsHostPanel: false } extensionPanel - ? extensionPanel.DisplayName.ToUpperInvariant() + ? extensionPanel.DisplayName : PanelDisplayNames.For(ActivePanelInfo?.PanelId ?? _activePanel ?? ""); /// @@ -781,7 +785,7 @@ } catch (Exception ex) { - _error = $"Failed to create notebook: {ex.Message}"; + _error = string.Format(UI.Error_CreateNotebook, ex.Message); } } @@ -802,7 +806,7 @@ } catch (Exception ex) { - _error = $"Failed to open file: {ex.Message}"; + _error = string.Format(UI.Error_OpenFile, ex.Message); } } @@ -833,7 +837,7 @@ } catch (Exception ex) { - _error = $"Failed to open file: {ex.Message}"; + _error = string.Format(UI.Error_OpenFile, ex.Message); } } @@ -872,7 +876,7 @@ _hasNativeHandle = true; if (Service is ServerNotebookService svc) svc.MarkSaved(); - await ShowStatusAsync($"Saved to {result.FileName}"); + await ShowStatusAsync(string.Format(UI.Status_SavedTo, result.FileName)); return; case "cancelled": return; @@ -880,7 +884,7 @@ // Fall through to modal break; default: - _error = $"Failed to save: {result.Message}"; + _error = string.Format(UI.Error_SaveFailed, result.Message); return; } } @@ -908,11 +912,11 @@ path = System.IO.Path.ChangeExtension(path, ".verso"); await Service.SaveAsync(path); - await ShowStatusAsync($"Saved to {System.IO.Path.GetFileName(path)}"); + await ShowStatusAsync(string.Format(UI.Status_SavedTo, System.IO.Path.GetFileName(path))); } catch (Exception ex) { - _error = $"Failed to save: {ex.Message}"; + _error = string.Format(UI.Error_SaveFailed, ex.Message); } } @@ -932,22 +936,24 @@ // clear the unsaved-changes flag here instead. if (Service is ServerNotebookService serverService) serverService.MarkSaved(); - await ShowStatusAsync($"Saved to {result.FileName}"); + await ShowStatusAsync(string.Format(UI.Status_SavedTo, result.FileName)); } else { - _error = $"Failed to save: {result.Message ?? "unknown error"}"; + _error = string.Format(UI.Error_SaveFailed, result.Message ?? UI.Error_Unknown); } } catch (Exception ex) { - _error = $"Failed to save: {ex.Message}"; + _error = string.Format(UI.Error_SaveFailed, ex.Message); } } private string BuildSuggestedFileName() { var title = Service.Title; + // Against the literal, not the resource: a new notebook stores this word in the + // file, so it is data rather than something the reader was shown. var name = string.IsNullOrWhiteSpace(title) || title == "Untitled" ? "notebook" : title; return name + ".verso"; @@ -1039,7 +1045,7 @@ } catch (Exception ex) { - _error = $"Cancel error: {ex.Message}"; + _error = string.Format(UI.Error_Cancel, ex.Message); } } @@ -1061,7 +1067,7 @@ { await executeAsync(); } - catch (Exception ex) when (ex is OperationCanceledException || ex.Message.Contains("operation was canceled", StringComparison.OrdinalIgnoreCase)) + catch (Exception ex) when (IsCancellation(ex)) { // User-initiated cancellation is not an error; the cell's status badge already shows it. } @@ -1069,7 +1075,7 @@ { await InvokeAsync(() => { - _error = $"Execution error: {ex.Message}"; + _error = string.Format(UI.Error_Execution, ex.Message); StateHasChanged(); }); } @@ -1414,7 +1420,7 @@ _selectedCellId = null; if (string.Equals(actionId, "verso.action.restart-kernel", StringComparison.OrdinalIgnoreCase)) - await ShowStatusAsync("Kernel restarted"); + await ShowStatusAsync(UI.Status_KernelRestarted); StateHasChanged(); } @@ -1435,7 +1441,7 @@ } catch (Exception ex) { - _diffErrorMessage = $"could not list comparison sources: {ex.Message}"; + _diffErrorMessage = string.Format(UI.Error_ListComparisonSources, ex.Message); } StateHasChanged(); @@ -1568,6 +1574,30 @@ } } + /// + /// Fills a sentence's placeholders with literal text drawn in a code style. The sentence is + /// one resource rather than the fragments between the placeholders, because word order moves + /// with the language while a file extension does not. + /// + private static MarkupString Sentence(string sentence, params string[] values) + => new(string.Format( + sentence, + values.Select(v => $"{System.Net.WebUtility.HtmlEncode(v)}").ToArray())); + + /// + /// Whether an exception is a cancellation the reader asked for, rather than a fault. + /// + /// + /// Cancellation reaches here wrapped in other exception types as well as on its own, so + /// the inner exception is checked too. The message is the last resort and not the first, + /// because the runtime writes it in the interface language: matching English text would + /// stop recognising a cancellation the moment somebody reads Verso in German. + /// + private static bool IsCancellation(Exception ex) + => ex is OperationCanceledException + || ex.InnerException is OperationCanceledException + || ex.Message.Contains("operation was canceled", StringComparison.OrdinalIgnoreCase); + private void HandleExtensionsUnavailable(IReadOnlyList unavailable) { _ = InvokeAsync(async () => await ShowStatusAsync(FormatUnavailableExtensions(unavailable))); @@ -1576,9 +1606,10 @@ private static string FormatUnavailableExtensions(IReadOnlyList unavailable) { var names = string.Join(", ", unavailable.Select(u => u.PackageId)); - var lead = unavailable.Count == 1 - ? $"Required extension {names} could not be loaded" - : $"{unavailable.Count} required extensions could not be loaded: {names}"; - return $"{lead}. Some layouts or cells may not work until it is available."; + // Two whole sentences rather than a shared tail, so the pronoun at the end can + // agree with the number at the start. + return unavailable.Count == 1 + ? string.Format(UI.Status_RequiredExtensionUnavailable_One, names) + : string.Format(UI.Status_RequiredExtensionUnavailable_Other, unavailable.Count, names); } } diff --git a/src/Verso.Blazor/Components/Routes.razor b/src/Verso.Blazor/Components/Routes.razor index c3971c0d..4b93d2b6 100644 --- a/src/Verso.Blazor/Components/Routes.razor +++ b/src/Verso.Blazor/Components/Routes.razor @@ -4,9 +4,9 @@ - Not found + @UI.NotFound_Title -

Sorry, there's nothing at this address.

+

@UI.NotFound_Message

diff --git a/src/Verso.Blazor/Components/_Imports.razor b/src/Verso.Blazor/Components/_Imports.razor index 79022a8d..e02ae6a9 100644 --- a/src/Verso.Blazor/Components/_Imports.razor +++ b/src/Verso.Blazor/Components/_Imports.razor @@ -14,5 +14,6 @@ @using Verso.Blazor.Shared.Components.Editor @using Verso.Blazor.Shared.Services @using Verso.Blazor.Shared.Models +@using Verso.Blazor.Shared.Resources @using Verso @using Verso.Abstractions diff --git a/src/Verso.Blazor/Localization/VersoLocalization.cs b/src/Verso.Blazor/Localization/VersoLocalization.cs new file mode 100644 index 00000000..6ec2eedd --- /dev/null +++ b/src/Verso.Blazor/Localization/VersoLocalization.cs @@ -0,0 +1,62 @@ +using System.Globalization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Localization; +using Verso.Localization; + +namespace Verso.Blazor.Localization; + +/// +/// Interface language for the server-hosted notebook UI. +/// +/// +/// Two entry points reach the same application: running Verso.Blazor directly, and +/// verso serve, which builds an equivalent host inside the CLI. They configure this the +/// same way from here rather than each growing their own copy. +/// +public static class VersoLocalization +{ + /// + /// Negotiates the interface language for each request, or pins it when one was asked for. + /// + /// + /// With no explicit language the browser decides through Accept-Language, which is the + /// right answer for a server someone else might open. An explicit language removes the + /// providers so every request gets it, which is the right answer for + /// verso serve --language de. + /// + /// Only the interface language moves. This process runs the kernels, so the formatting + /// culture is pinned to whatever the process started with: a menu language must not decide + /// how a cell's own results are written out. + /// + /// Call this before the components are mapped. A Blazor circuit takes its culture from the + /// request that opened it, so anything registered afterwards arrives too late. + /// + /// The application being configured. + /// A language tag from the command line, or null. + public static WebApplication UseVersoLocalization(this WebApplication app, string? requestedLanguage) + { + var pinned = VersoCultures.TryMatch(requestedLanguage, out var explicitCulture); + var uiCulture = pinned ? explicitCulture : VersoCultures.Resolve(null); + var formattingCulture = CultureInfo.CurrentCulture; + + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture(formattingCulture, uiCulture), + ApplyCurrentCultureToResponseHeaders = true, + }; + + // One supported formatting culture means header negotiation can never move it: anything a + // browser asks for falls back to the default, which is the process culture. + options.SupportedCultures = new List { formattingCulture }; + options.SupportedUICultures = VersoCultures.Supported + .Select(tag => new CultureInfo(tag)) + .Append(new CultureInfo(VersoCultures.Pseudo)) + .ToList(); + + if (pinned) + options.RequestCultureProviders.Clear(); + + app.UseRequestLocalization(options); + return app; + } +} diff --git a/src/Verso.Blazor/Program.cs b/src/Verso.Blazor/Program.cs index ec9745e1..d0e1f93c 100644 --- a/src/Verso.Blazor/Program.cs +++ b/src/Verso.Blazor/Program.cs @@ -1,3 +1,4 @@ +using Verso.Blazor.Localization; using Verso.Blazor.Services; using Verso.Blazor.Shared.Services; using Verso.Extensions; @@ -37,6 +38,11 @@ } app.UseHttpsRedirection(); + +// Before the components are mapped: a circuit takes its culture from the request that opened it. +// --language pins the interface language; without it the browser's Accept-Language decides. +app.UseVersoLocalization(builder.Configuration["language"]); + app.UseStaticFiles(); app.UseAntiforgery(); diff --git a/src/Verso.Blazor/Services/GitCliHelper.cs b/src/Verso.Blazor/Services/GitCliHelper.cs index 57ae2c06..09f31caa 100644 --- a/src/Verso.Blazor/Services/GitCliHelper.cs +++ b/src/Verso.Blazor/Services/GitCliHelper.cs @@ -1,5 +1,7 @@ using System.Diagnostics; using System.Text; +using Verso.Blazor.Shared.Models; +using Verso.Blazor.Shared.Resources; namespace Verso.Blazor.Services; @@ -40,12 +42,12 @@ public static (string Content, string Label) Show(string filePath, string refNam var directory = Path.GetDirectoryName(fullPath); if (directory is null) { - throw new InvalidOperationException($"Could not resolve the directory of '{filePath}'."); + throw new InvalidOperationException(string.Format(UI.Compare_DirectoryUnresolved, filePath)); } if (FindRepoRoot(directory) is null) { - throw new InvalidOperationException("This notebook is not inside a git repository."); + throw new InvalidOperationException(UI.Compare_NotInGitRepo); } // The "ref:./name" form makes git resolve the repo-relative path itself, from the @@ -58,15 +60,15 @@ public static (string Content, string Label) Show(string filePath, string refNam { var detail = stderr.Contains("exists on disk, but not in", StringComparison.OrdinalIgnoreCase) || stderr.Contains("does not exist in", StringComparison.OrdinalIgnoreCase) - ? $"'{fileName}' is not tracked at '{refName}'. Commit the file first, or pick a different ref." + ? string.Format(UI.Compare_FileNotTracked, fileName, refName) : stderr.Contains("unknown revision", StringComparison.OrdinalIgnoreCase) || stderr.Contains("invalid object name", StringComparison.OrdinalIgnoreCase) - ? $"'{refName}' is not a known branch, tag, or commit." - : $"git could not read '{fileName}' at '{refName}'."; + ? string.Format(UI.Compare_UnknownRef, refName) + : string.Format(UI.Compare_GitReadFailed, fileName, refName); throw new InvalidOperationException(detail); } - return (stdout, $"Git: {refName}"); + return (stdout, DiffSources.ResolvedName(DiffSources.GitRef, refName)); } private static (int ExitCode, string Stdout, string Stderr) Run(string workingDirectory, params string[] arguments) diff --git a/src/Verso.Blazor/Services/ServerNotebookService.Diff.cs b/src/Verso.Blazor/Services/ServerNotebookService.Diff.cs index 11ac2d0f..c321e179 100644 --- a/src/Verso.Blazor/Services/ServerNotebookService.Diff.cs +++ b/src/Verso.Blazor/Services/ServerNotebookService.Diff.cs @@ -2,6 +2,7 @@ using Verso.Blazor.Shared.Models; using Verso.Diffing; using Verso.Serializers; +using Verso.Blazor.Shared.Resources; namespace Verso.Blazor.Services; @@ -18,15 +19,18 @@ public Task> GetDiffSourcesAsync() && Path.GetDirectoryName(Path.GetFullPath(_filePath!)) is { } directory && GitCliHelper.FindRepoRoot(directory) is not null; + // Named through DiffSources so this host and the embedded one offer the same four + // baselines under the same words, and a rename cannot reach one without the other. + static DiffSourceInfo Describe(string id, string kind, bool available) => + new(id, DiffSources.NameOf(id)!, kind, available, + available ? null : DiffSources.UnavailableReason(id)); + IReadOnlyList sources = new List { - new("lastSaved", "Last Saved", "lastSaved", hasFilePath, - hasFilePath ? null : "The notebook has not been saved to a file yet."), - new("gitHead", "Git: HEAD", "git", inGitRepo, - inGitRepo ? null : "The notebook file is not inside a git repository."), - new("gitRef", "Git: Compare with Ref...", "git", inGitRepo, - inGitRepo ? null : "The notebook file is not inside a git repository."), - new("file", "Choose File...", "file", true), + Describe(DiffSources.LastSaved, "lastSaved", hasFilePath), + Describe(DiffSources.GitHead, "git", inGitRepo), + Describe(DiffSources.GitRef, "git", inGitRepo), + Describe(DiffSources.File, "file", true), }; return Task.FromResult(sources); } @@ -35,18 +39,18 @@ public Task> GetDiffSourcesAsync() { if (_scaffold is null) { - throw new InvalidOperationException("No notebook is open."); + throw new InvalidOperationException(UI.Common_NoNotebookOpen); } var (content, baselinePath, label) = sourceId switch { - "lastSaved" => await ReadLastSavedBaselineAsync(), - "gitHead" => ReadGitBaseline("HEAD"), - "gitRef" => ReadGitBaseline( + DiffSources.LastSaved => await ReadLastSavedBaselineAsync(), + DiffSources.GitHead => ReadGitBaseline("HEAD"), + DiffSources.GitRef => ReadGitBaseline( !string.IsNullOrWhiteSpace(explicitInput) ? explicitInput.Trim() : throw new InvalidOperationException("A git ref is required to compare with a ref.")), - "file" => await ReadFileBaselineAsync( + DiffSources.File => await ReadFileBaselineAsync( !string.IsNullOrWhiteSpace(explicitInput) ? explicitInput.Trim() : throw new InvalidOperationException("A file path is required to compare with a file.")), @@ -73,23 +77,23 @@ public Task> GetDiffSourcesAsync() { if (string.IsNullOrEmpty(_filePath)) { - throw new InvalidOperationException("The notebook has not been saved to a file yet."); + throw new InvalidOperationException(UI.Compare_NotSavedYet); } if (!File.Exists(_filePath)) { - throw new InvalidOperationException($"'{_filePath}' does not exist on disk."); + throw new InvalidOperationException(string.Format(UI.Compare_FileMissing, _filePath)); } var content = await File.ReadAllTextAsync(_filePath); - return (content, _filePath, "Last Saved"); + return (content, _filePath, DiffSources.ResolvedName(DiffSources.LastSaved, null)); } private (string Content, string BaselinePath, string Label) ReadGitBaseline(string refName) { if (string.IsNullOrEmpty(_filePath)) { - throw new InvalidOperationException("The notebook has not been saved to a file yet."); + throw new InvalidOperationException(UI.Compare_NotSavedYet); } var (content, label) = GitCliHelper.Show(_filePath, refName); @@ -100,11 +104,11 @@ public Task> GetDiffSourcesAsync() { if (!File.Exists(path)) { - throw new InvalidOperationException($"'{path}' does not exist."); + throw new InvalidOperationException(string.Format(UI.Compare_FileMissing, path)); } var content = await File.ReadAllTextAsync(path); - return (content, path, Path.GetFileName(path)); + return (content, path, DiffSources.ResolvedName(DiffSources.File, Path.GetFileName(path))); } /// @@ -127,7 +131,7 @@ private async Task DeserializeBaselineAsync(string content, strin catch (Exception ex) { throw new InvalidOperationException( - $"Could not parse '{Path.GetFileName(baselinePath)}' as a notebook: {ex.Message}", ex); + string.Format(UI.Compare_ParseFailed, Path.GetFileName(baselinePath), ex.Message), ex); } if (_extensionHost is not null) diff --git a/src/Verso.Blazor/Services/ServerNotebookService.Marketplace.cs b/src/Verso.Blazor/Services/ServerNotebookService.Marketplace.cs index 7c76004c..a17fd97e 100644 --- a/src/Verso.Blazor/Services/ServerNotebookService.Marketplace.cs +++ b/src/Verso.Blazor/Services/ServerNotebookService.Marketplace.cs @@ -2,6 +2,7 @@ using Verso.Blazor.Shared.Models; using Verso.Extensions; using Verso.Extensions.Marketplace; +using Verso.Blazor.Shared.Resources; namespace Verso.Blazor.Services; @@ -48,16 +49,16 @@ public async Task InstallExtensionAsync( string packageId, string? version, CancellationToken ct) { if (_scaffold is null || _extensionHost is null) - return new PackageInstallResultDto(false, null, "No notebook is open.", 0); + return new PackageInstallResultDto(false, null, UI.Common_NoNotebookOpen, 0); try { if (!_trustStore.IsApproved(packageId, version)) { - var consent = new List { new(packageId, version, "marketplace") }; + var consent = new List { new(packageId, version, Verso.Resources.Strings.Consent_Source_Marketplace) }; var approved = await _extensionHost.RequestExtensionConsentAsync(consent, ct); if (!approved) - return new PackageInstallResultDto(false, null, "Installation was not approved.", 0); + return new PackageInstallResultDto(false, null, UI.Marketplace_InstallNotApproved, 0); _trustStore.Approve(packageId, version); _trustStore.Save(); @@ -106,7 +107,7 @@ public async Task InstallLocalExtensionAsync( string fileName, Stream content, CancellationToken ct) { if (_scaffold is null || _extensionHost is null) - return new PackageInstallResultDto(false, null, "No notebook is open.", 0); + return new PackageInstallResultDto(false, null, UI.Common_NoNotebookOpen, 0); var tempDir = Directory.CreateTempSubdirectory("verso-sideload"); try diff --git a/src/Verso.Blazor/Services/ServerNotebookService.cs b/src/Verso.Blazor/Services/ServerNotebookService.cs index a9e09bcd..75abf973 100644 --- a/src/Verso.Blazor/Services/ServerNotebookService.cs +++ b/src/Verso.Blazor/Services/ServerNotebookService.cs @@ -279,7 +279,9 @@ public IReadOnlyList AvailableCellTypes { get { - var types = new List { new("code", "Code") }; + // Code has no ICellType of its own: it is what a cell is when nothing else claims + // it, so the name comes from the engine's resources rather than a registration. + var types = new List { new("code", Verso.Resources.Strings.CellType_Code) }; if (_extensionHost is null) return types; var hasMarkdown = _extensionHost.GetCellTypes() diff --git a/src/Verso.Cli/Commands/ConvertCommand.cs b/src/Verso.Cli/Commands/ConvertCommand.cs index 1a1cc001..da58a1c4 100644 --- a/src/Verso.Cli/Commands/ConvertCommand.cs +++ b/src/Verso.Cli/Commands/ConvertCommand.cs @@ -1,4 +1,5 @@ using System.CommandLine; +using Verso.Cli.Resources; using Verso.Cli.Utilities; using Verso.Extensions; @@ -11,9 +12,9 @@ public static class ConvertCommand { public static Command Create() { - var inputArg = new Argument("input", "Path to the source notebook file."); + var inputArg = new Argument("input", Strings.Arg_InputNotebook); - var toOption = new Option("--to", "Target format: verso, ipynb, md, or dib.") + var toOption = new Option("--to", Strings.Convert_OptTo) { IsRequired = true }; @@ -21,19 +22,17 @@ public static Command Create() { var value = result.GetValueForOption(toOption); if (value is not ("verso" or "ipynb" or "md" or "dib")) - result.ErrorMessage = $"Unsupported format '{value}'. Supported: verso, ipynb, md, dib"; + result.ErrorMessage = string.Format( + Strings.Convert_InvalidTarget, value, "verso, ipynb, md, dib"); }); - var outputOption = new Option("--output", - "Output file path. Defaults to input filename with the new extension."); + var outputOption = new Option("--output", Strings.Convert_OptOutput); - var stripOutputsOption = new Option("--strip-outputs", () => false, - "Remove all cell outputs from the converted notebook."); + var stripOutputsOption = new Option("--strip-outputs", () => false, Strings.Convert_OptStripOutputs); - var extensionsOption = new Option("--extensions", - "Directory to scan for additional extension assemblies."); + var extensionsOption = new Option("--extensions", Strings.Option_Extensions); - var command = new Command("convert", "Convert between notebook formats.") + var command = new Command("convert", Strings.Convert_Description) { inputArg, toOption, @@ -53,7 +52,8 @@ public static Command Create() var inputPath = Path.GetFullPath(input.FullName); if (!File.Exists(inputPath)) { - Console.Error.WriteLine($"Error: Input file not found: {inputPath}"); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Error_InputNotFound, inputPath))); context.ExitCode = ExitCodes.FileNotFound; return; } @@ -76,7 +76,7 @@ public static Command Create() } catch (SerializerNotFoundException ex) { - Console.Error.WriteLine($"Error: {ex.Message}"); + Console.Error.WriteLine(Messages.Error(ex.Message)); context.ExitCode = ExitCodes.SerializationError; return; } @@ -89,7 +89,7 @@ public static Command Create() } catch (SerializerNotFoundException ex) { - Console.Error.WriteLine($"Error: {ex.Message}"); + Console.Error.WriteLine(Messages.Error(ex.Message)); context.ExitCode = ExitCodes.SerializationError; return; } @@ -103,7 +103,8 @@ public static Command Create() } catch (Exception ex) { - Console.Error.WriteLine($"Error: Failed to deserialize '{inputPath}': {ex.Message}"); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Error_DeserializeFailed, inputPath, ex.Message))); context.ExitCode = ExitCodes.SerializationError; return; } @@ -122,7 +123,8 @@ public static Command Create() } catch (Exception ex) { - Console.Error.WriteLine($"Error: Post-processing failed: {ex.Message}"); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Convert_PostProcessingFailed, ex.Message))); context.ExitCode = ExitCodes.SerializationError; return; } @@ -142,13 +144,15 @@ public static Command Create() } catch (NotSupportedException) { - Console.Error.WriteLine($"Error: Converting to '{to}' format is not yet supported."); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Convert_NotSupported, to))); context.ExitCode = ExitCodes.SerializationError; return; } catch (Exception ex) { - Console.Error.WriteLine($"Error: Serialization failed: {ex.Message}"); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Convert_SerializationFailed, ex.Message))); context.ExitCode = ExitCodes.SerializationError; return; } @@ -159,12 +163,12 @@ public static Command Create() : Path.ChangeExtension(inputPath, outputSerializer.FileExtensions[0]); await File.WriteAllTextAsync(outputPath, serialized); - Console.WriteLine($"Converted '{inputPath}' -> '{outputPath}'"); + Console.WriteLine(string.Format(Strings.Convert_Done, inputPath, outputPath)); context.ExitCode = ExitCodes.Success; } catch (Exception ex) { - Console.Error.WriteLine($"Error: {ex.Message}"); + Console.Error.WriteLine(Messages.Error(ex.Message)); context.ExitCode = ExitCodes.CellFailure; } finally diff --git a/src/Verso.Cli/Commands/ExportCommand.cs b/src/Verso.Cli/Commands/ExportCommand.cs index c0dbd436..08937b54 100644 --- a/src/Verso.Cli/Commands/ExportCommand.cs +++ b/src/Verso.Cli/Commands/ExportCommand.cs @@ -1,6 +1,7 @@ using System.CommandLine; using Verso.Abstractions; using Verso.Cli.Execution; +using Verso.Cli.Resources; using Verso.Cli.Utilities; using Verso.Contexts; using Verso.Execution; @@ -19,36 +20,28 @@ public static class ExportCommand { public static Command Create() { - var inputArg = new Argument("input", "Path to the source notebook file.") + var inputArg = new Argument("input", Strings.Arg_InputNotebook) { Arity = ArgumentArity.ZeroOrOne }; - var formatOption = new Option(new[] { "--format", "-f" }, - "Export format, matched against the DisplayName of a registered IToolbarAction whose placement is ExportMenu. Case-insensitive. Quote values containing whitespace. Use --list to see installed formats."); + var formatOption = new Option(new[] { "--format", "-f" }, Strings.Export_OptFormat); - var outputOption = new Option(new[] { "--output", "-o" }, - "Output file path. If omitted, the exporter's suggested filename is written to the current directory."); + var outputOption = new Option(new[] { "--output", "-o" }, Strings.Export_OptOutput); - var executeOption = new Option(new[] { "--execute", "-x" }, () => false, - "Execute the notebook before exporting so stored outputs are refreshed."); + var executeOption = new Option(new[] { "--execute", "-x" }, () => false, Strings.Export_OptExecute); - var layoutOption = new Option("--layout", - "Layout id to apply during export, exposed as ActiveLayoutId on the action context."); + var layoutOption = new Option("--layout", Strings.Export_OptLayout); - var themeOption = new Option("--theme", - "DisplayName of a registered theme, matched case-insensitively. Quote values with whitespace. ThemeId is accepted as a fallback to disambiguate display-name collisions. Use --list-themes to see installed themes."); + var themeOption = new Option("--theme", Strings.Export_OptTheme); - var extensionsOption = new Option("--extensions", - "Directory to scan for additional extension assemblies."); + var extensionsOption = new Option("--extensions", Strings.Option_Extensions); - var listOption = new Option("--list", () => false, - "List registered export actions (DisplayName, ActionId, Description) and exit."); + var listOption = new Option("--list", () => false, Strings.Export_OptList); - var listThemesOption = new Option("--list-themes", () => false, - "List registered themes (DisplayName, Kind, Description) and exit."); + var listThemesOption = new Option("--list-themes", () => false, Strings.Export_OptListThemes); - var command = new Command("export", "Export a notebook via an ExportMenu toolbar action.") + var command = new Command("export", Strings.Export_Description) { inputArg, formatOption, @@ -105,14 +98,15 @@ public static Command Create() if (input is null) { - Console.Error.WriteLine("Error: is required unless --list is specified."); + Console.Error.WriteLine(Messages.Error(Strings.Export_InputRequired)); context.ExitCode = ExitCodes.FileNotFound; return; } if (string.IsNullOrWhiteSpace(format)) { - Console.Error.WriteLine("Error: --format is required. Use 'verso export --list' to see available formats."); + Console.Error.WriteLine(Messages.Error(Strings.Export_FormatRequired) + " " + + string.Format(Strings.Hint_RunForFormats, ListFormatsCommand)); context.ExitCode = ExitCodes.SerializationError; return; } @@ -120,7 +114,8 @@ public static Command Create() var inputPath = Path.GetFullPath(input.FullName); if (!File.Exists(inputPath)) { - Console.Error.WriteLine($"Error: Input file not found: {inputPath}"); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Error_InputNotFound, inputPath))); context.ExitCode = ExitCodes.FileNotFound; return; } @@ -132,7 +127,7 @@ public static Command Create() } catch (SerializerNotFoundException ex) { - Console.Error.WriteLine($"Error: {ex.Message}"); + Console.Error.WriteLine(Messages.Error(ex.Message)); context.ExitCode = ExitCodes.SerializationError; return; } @@ -145,7 +140,8 @@ public static Command Create() } catch (Exception ex) { - Console.Error.WriteLine($"Error: Failed to deserialize '{inputPath}': {ex.Message}"); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Error_DeserializeFailed, inputPath, ex.Message))); context.ExitCode = ExitCodes.SerializationError; return; } @@ -164,7 +160,7 @@ public static Command Create() if (HasAnyFailure(notebook, results)) { - Console.Error.WriteLine("Error: Notebook execution reported errors. Aborting export."); + Console.Error.WriteLine(Messages.Error(Strings.Export_ExecutionErrors)); context.ExitCode = ExitCodes.CellFailure; return; } @@ -172,7 +168,8 @@ public static Command Create() if (!ToolbarActionResolver.TryResolveAction(extensionHost, format, out var action, out var resolveError)) { - Console.Error.WriteLine(resolveError + " Run 'verso export --list' to see available formats."); + Console.Error.WriteLine(resolveError + " " + + string.Format(Strings.Hint_RunForFormats, ListFormatsCommand)); context.ExitCode = ExitCodes.SerializationError; return; } @@ -182,7 +179,8 @@ public static Command Create() { if (!ToolbarActionResolver.TryResolveTheme(extensionHost, themeText, out selectedTheme, out var themeError)) { - Console.Error.WriteLine(themeError + " Run 'verso export --list-themes' for details."); + Console.Error.WriteLine(themeError + " " + + string.Format(Strings.Hint_RunForDetails, ListThemesCommand)); context.ExitCode = ExitCodes.SerializationError; return; } @@ -204,17 +202,18 @@ public static Command Create() if (ctx.WrittenPath is null) { - Console.Error.WriteLine($"Error: Export action '{action.ActionId}' did not produce a file."); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Export_NoFileProduced, action.ActionId))); context.ExitCode = ExitCodes.CellFailure; return; } - Console.WriteLine($"Exported '{inputPath}' -> '{ctx.WrittenPath}'"); + Console.WriteLine(string.Format(Strings.Export_Done, inputPath, ctx.WrittenPath)); context.ExitCode = ExitCodes.Success; } catch (Exception ex) { - Console.Error.WriteLine($"Error: {ex.Message}"); + Console.Error.WriteLine(Messages.Error(ex.Message)); context.ExitCode = ExitCodes.CellFailure; } finally @@ -232,6 +231,12 @@ public static Command Create() return command; } + /// What to type to see the formats that are installed. The same in every language. + private const string ListFormatsCommand = "verso export --list"; + + /// What to type to see the themes that are installed. The same in every language. + private const string ListThemesCommand = "verso export --list-themes"; + private static void PrintExportActions(ExtensionHost extensionHost) { var actions = extensionHost.GetToolbarActions() @@ -242,17 +247,20 @@ private static void PrintExportActions(ExtensionHost extensionHost) if (actions.Count == 0) { - Console.WriteLine("No export actions are registered."); + Console.WriteLine(Strings.Export_NoActions); return; } - var nameWidth = Math.Max("FORMAT".Length, actions.Max(a => a.DisplayName.Length)); + // Read once into a local: a column is padded to the width of its own heading, and the + // heading is not the length it was in English. + var formatHeading = Strings.Table_Format; + var nameWidth = Math.Max(DisplayWidth.Measure(formatHeading), actions.Max(a => DisplayWidth.Measure(a.DisplayName))); - Console.WriteLine($"{"FORMAT".PadRight(nameWidth)} DESCRIPTION"); + Console.WriteLine($"{DisplayWidth.PadRight(formatHeading, nameWidth)} {Strings.Table_Description}"); foreach (var action in actions) { var description = action.Description ?? string.Empty; - Console.WriteLine($"{action.DisplayName.PadRight(nameWidth)} {description}"); + Console.WriteLine($"{DisplayWidth.PadRight(action.DisplayName, nameWidth)} {description}"); } } @@ -264,18 +272,20 @@ private static void PrintThemes(ExtensionHost extensionHost) if (themes.Count == 0) { - Console.WriteLine("No themes are registered."); + Console.WriteLine(Strings.List_NoThemes); return; } - var nameWidth = Math.Max("THEME".Length, themes.Max(t => t.DisplayName.Length)); - var kindWidth = Math.Max("KIND".Length, themes.Max(t => t.ThemeKind.ToString().Length)); + var themeHeading = Strings.Table_Theme; + var kindHeading = Strings.Table_Kind; + var nameWidth = Math.Max(DisplayWidth.Measure(themeHeading), themes.Max(t => DisplayWidth.Measure(t.DisplayName))); + var kindWidth = Math.Max(DisplayWidth.Measure(kindHeading), themes.Max(t => t.ThemeKind.ToString().Length)); - Console.WriteLine($"{"THEME".PadRight(nameWidth)} {"KIND".PadRight(kindWidth)} DESCRIPTION"); + Console.WriteLine($"{DisplayWidth.PadRight(themeHeading, nameWidth)} {DisplayWidth.PadRight(kindHeading, kindWidth)} {Strings.Table_Description}"); foreach (var theme in themes) { var description = theme.Description ?? string.Empty; - Console.WriteLine($"{theme.DisplayName.PadRight(nameWidth)} {theme.ThemeKind.ToString().PadRight(kindWidth)} {description}"); + Console.WriteLine($"{DisplayWidth.PadRight(theme.DisplayName, nameWidth)} {theme.ThemeKind.ToString().PadRight(kindWidth)} {description}"); } } diff --git a/src/Verso.Cli/Commands/InfoCommand.cs b/src/Verso.Cli/Commands/InfoCommand.cs index 5d050caa..cc940bbd 100644 --- a/src/Verso.Cli/Commands/InfoCommand.cs +++ b/src/Verso.Cli/Commands/InfoCommand.cs @@ -1,5 +1,7 @@ using System.CommandLine; using System.Reflection; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; using Verso.Extensions; namespace Verso.Cli.Commands; @@ -12,7 +14,7 @@ public static class InfoCommand { public static Command Create() { - var command = new Command("info", "Display Verso CLI version, runtime, and extension information."); + var command = new Command("info", Strings.Info_Description); command.SetHandler(ExecuteAsync); return command; } @@ -34,14 +36,20 @@ private static async Task ExecuteAsync() { await extensionHost.LoadBuiltInExtensionsAsync(); + // Padded from the labels themselves rather than by counting spaces into each line, + // because a translated label is not the length the English one was. + var labelWidth = Math.Max( + DisplayWidth.Measure(Strings.Info_LabelRuntime), + DisplayWidth.Measure(Strings.Info_LabelEngine)) + 4; + Console.WriteLine($"Verso CLI {cliVersion}"); - Console.WriteLine($"Runtime: .NET {Environment.Version}"); - Console.WriteLine($"Engine: Verso {engineVersion}"); + Console.WriteLine($"{DisplayWidth.PadRight(Strings.Info_LabelRuntime, labelWidth)}.NET {Environment.Version}"); + Console.WriteLine($"{DisplayWidth.PadRight(Strings.Info_LabelEngine, labelWidth)}Verso {engineVersion}"); var kernels = extensionHost.GetKernels(); if (kernels.Count > 0) { - Console.WriteLine("Extensions:"); + Console.WriteLine(Strings.Info_HeadingExtensions); foreach (var kernel in kernels) { Console.WriteLine($" {kernel.ExtensionId,-28} {kernel.Name,-24} {kernel.Version}"); @@ -51,7 +59,7 @@ private static async Task ExecuteAsync() var serializers = extensionHost.GetSerializers(); if (serializers.Count > 0) { - Console.WriteLine("Serializers:"); + Console.WriteLine(Strings.Info_HeadingSerializers); foreach (var serializer in serializers) { var extensions = string.Join(", ", serializer.FileExtensions); @@ -62,7 +70,7 @@ private static async Task ExecuteAsync() var formatters = extensionHost.GetFormatters(); if (formatters.Count > 0) { - Console.WriteLine("Formatters:"); + Console.WriteLine(Strings.Info_HeadingFormatters); foreach (var formatter in formatters) { Console.WriteLine($" {formatter.ExtensionId,-28} {formatter.Name,-24} {formatter.Version}"); diff --git a/src/Verso.Cli/Commands/ReplCommand.cs b/src/Verso.Cli/Commands/ReplCommand.cs index b7190f95..36d9cb6d 100644 --- a/src/Verso.Cli/Commands/ReplCommand.cs +++ b/src/Verso.Cli/Commands/ReplCommand.cs @@ -6,6 +6,7 @@ using Verso.Cli.Repl.Rendering; using Verso.Cli.Repl.Settings; using Verso.Cli.Repl.Signals; +using Verso.Cli.Resources; using Verso.Cli.Utilities; using Verso.Execution; using Verso.Extensions; @@ -32,48 +33,36 @@ public static class ReplCommand { public static Command Create() { - var notebookArg = new Argument("notebook", - "Path to a .verso, .ipynb, or .dib file. When omitted, starts with an empty scratch notebook.") + var notebookArg = new Argument("notebook", Strings.Repl_ArgNotebook) { Arity = ArgumentArity.ZeroOrOne }; - var kernelOption = new Option("--kernel", - "Active kernel for the first cell. Matched against ILanguageKernel.KernelId case-insensitively. Can be changed at runtime with .kernel."); + var kernelOption = new Option("--kernel", Strings.Repl_OptKernel); - var executeOption = new Option(new[] { "--execute", "-x" }, () => false, - "When combined with , executes all loaded cells before handing control to the prompt."); + var executeOption = new Option(new[] { "--execute", "-x" }, () => false, Strings.Repl_OptExecute); - var themeOption = new Option("--theme", - "Active theme for output rendering. DisplayName case-insensitive with ThemeId fallback."); + var themeOption = new Option("--theme", Strings.Repl_OptTheme); - var layoutOption = new Option("--layout", - "Default layout id, passed to .export as ActiveLayoutId unless the command overrides it."); + var layoutOption = new Option("--layout", Strings.Repl_OptLayout); - var extensionsOption = new Option("--extensions", - "Additional directory to scan for extension assemblies."); + var extensionsOption = new Option("--extensions", Strings.Option_Extensions); - var noColorOption = new Option("--no-color", () => false, - "Disable ANSI styling. Output is plain UTF-8 text."); + var noColorOption = new Option("--no-color", () => false, Strings.Repl_OptNoColor); - var plainOption = new Option("--plain", () => false, - "Force the line-oriented fallback prompt, bypassing PrettyPrompt even when the terminal would support it."); + var plainOption = new Option("--plain", () => false, Strings.Repl_OptPlain); - var historyOption = new Option("--history", - "Path to the prompt history file. Use 'none' to disable persistent history."); + var historyOption = new Option("--history", Strings.Repl_OptHistory); - var listKernelsOption = new Option("--list-kernels", () => false, - "Print available kernels and exit."); + var listKernelsOption = new Option("--list-kernels", () => false, Strings.Repl_OptListKernels); - var listThemesOption = new Option("--list-themes", () => false, - "Print registered themes and exit."); + var listThemesOption = new Option("--list-themes", () => false, Strings.Repl_OptListThemes); - var preserveFormatOption = new Option("--preserve-format", () => false, - "When the loaded notebook is .ipynb, .save (no arg) writes back to .ipynb instead of converting to .verso. Cell outputs are preserved."); + var preserveFormatOption = new Option("--preserve-format", () => false, Strings.Repl_OptPreserveFormat); var pythonOption = PythonInterpreterOption.Create(); - var command = new Command("repl", "Start an interactive Verso REPL in the terminal.") + var command = new Command("repl", Strings.Repl_Description) { notebookArg, kernelOption, @@ -168,7 +157,8 @@ internal static async Task RunAsync(ReplOptions options, bool listKernels, var fullPath = Path.GetFullPath(options.NotebookPath); if (!File.Exists(fullPath)) { - Console.Error.WriteLine($"Error: Notebook file not found: {fullPath}"); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Error_NotebookNotFound, fullPath))); return ExitCodes.FileNotFound; } @@ -179,7 +169,7 @@ internal static async Task RunAsync(ReplOptions options, bool listKernels, } catch (SerializerNotFoundException ex) { - Console.Error.WriteLine($"Error: {ex.Message}"); + Console.Error.WriteLine(Messages.Error(ex.Message)); return ExitCodes.SerializationError; } @@ -190,7 +180,8 @@ internal static async Task RunAsync(ReplOptions options, bool listKernels, } catch (Exception ex) { - Console.Error.WriteLine($"Error: Failed to deserialize '{fullPath}': {ex.Message}"); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Error_DeserializeFailed, fullPath, ex.Message))); return ExitCodes.SerializationError; } @@ -200,7 +191,8 @@ internal static async Task RunAsync(ReplOptions options, bool listKernels, { notebook = new NotebookModel { - Title = $"Verso REPL — {DateTimeOffset.Now:yyyy-MM-ddTHH-mm-ss}" + Title = string.Format( + Strings.Repl_ScratchTitle, DateTimeOffset.Now.ToString("yyyy-MM-ddTHH-mm-ss")) }; } @@ -287,7 +279,7 @@ internal static async Task RunAsync(ReplOptions options, bool listKernels, var result = cell.LastStatus switch { "Success" => ExecutionResult.Success(cell.Id, cell.ExecutionCount ?? 0, cell.LastElapsed ?? TimeSpan.Zero), - "Failed" => ExecutionResult.Failed(cell.Id, cell.ExecutionCount ?? 0, cell.LastElapsed ?? TimeSpan.Zero, new Exception("Prior execution failed.")), + "Failed" => ExecutionResult.Failed(cell.Id, cell.ExecutionCount ?? 0, cell.LastElapsed ?? TimeSpan.Zero, new Exception(Strings.Repl_PriorExecutionFailed)), "Cancelled" => ExecutionResult.Cancelled(cell.Id, cell.ExecutionCount ?? 0, cell.LastElapsed ?? TimeSpan.Zero), _ => ExecutionResult.Success(cell.Id, 0, TimeSpan.Zero) }; @@ -307,7 +299,7 @@ internal static async Task RunAsync(ReplOptions options, bool listKernels, } catch (Exception ex) { - Console.Error.WriteLine($"Fatal error: {ex.Message}"); + Console.Error.WriteLine(Messages.Fatal(ex.Message)); return ExitCodes.CellFailure; } finally @@ -341,10 +333,20 @@ private static IAnsiConsole BuildConsole(bool useColor) private static void PrintHeader(IAnsiConsole console, bool useColor, ReplSession session, ExtensionHost extensionHost) { - var kernel = session.ActiveKernelId ?? ""; - var theme = session.ActiveTheme?.DisplayName ?? ""; - var notebookLine = session.NotebookPath is not null ? session.NotebookPath : "*scratch*"; - var extensionCount = extensionHost.GetExtensionInfos().Count; + var kernel = session.ActiveKernelId ?? Strings.Repl_MarkerNone; + var theme = session.ActiveTheme?.DisplayName ?? Strings.Repl_MarkerDefault; + var notebookLine = session.NotebookPath is not null ? session.NotebookPath : Strings.Repl_MarkerScratch; + var extensions = string.Format(Strings.Repl_HeaderExtensionCount, extensionCountOf(extensionHost)); + var hint = Messages.Typed(Strings.Repl_HeaderHint, HelpCommand, ExitCommand); + + // Read into locals and measured here, because a translated label is not the length the + // English one was and the four of them are meant to line up. + var labels = new[] + { + Strings.Repl_HeaderKernel, Strings.Repl_HeaderTheme, + Strings.Repl_HeaderNotebook, Strings.Repl_HeaderExtensions, + }; + var labelWidth = labels.Max(DisplayWidth.Measure); if (useColor) { @@ -354,32 +356,42 @@ private static void PrintHeader(IAnsiConsole console, bool useColor, ReplSession console.Write(banner); console.WriteLine(); - var panel = new Panel( - $"[bold]kernel:[/] {Markup.Escape(kernel)}\n" + - $"[bold]theme:[/] {Markup.Escape(theme)}\n" + - $"[bold]notebook:[/] {Markup.Escape(notebookLine)}\n" + - $"[bold]extensions:[/] {extensionCount} loaded") + var values = new[] { kernel, theme, notebookLine, extensions }; + var panel = new Panel(string.Join("\n", labels.Zip(values, (label, value) => + $"[bold]{Markup.Escape(label)}[/] {Markup.Escape(value)}"))) { Border = BoxBorder.Rounded, BorderStyle = new Style(foreground: Color.Grey39) }; console.Write(panel); - console.MarkupLine("[dim]Type [bold].help[/] for commands, [bold].exit[/] to quit.[/]"); + console.MarkupLine(Messages.In("dim", hint)); console.WriteLine(); } else { console.WriteLine("Verso REPL"); - console.WriteLine($" kernel: {kernel}"); - console.WriteLine($" theme: {theme}"); - console.WriteLine($" notebook: {notebookLine}"); - console.WriteLine($" extensions: {extensionCount} loaded"); + foreach (var (label, value) in labels.Zip(new[] { kernel, theme, notebookLine, extensions })) + console.WriteLine($" {DisplayWidth.PadRight(label, labelWidth)} {value}"); console.WriteLine(); - console.WriteLine("Type .help for commands, .exit to quit."); + console.MarkupLine(hint); console.WriteLine(); } + + static int extensionCountOf(ExtensionHost host) => host.GetExtensionInfos().Count; } + /// What the reader types to see the REPL's commands. The same in every language. + private const string HelpCommand = ".help"; + + /// What the reader types to leave the REPL. The same in every language. + private const string ExitCommand = ".exit"; + + /// What to type to see the kernels that are installed. The same in every language. + private const string ListKernelsCommand = "verso repl --list-kernels"; + + /// What to type to see the themes that are installed. The same in every language. + private const string ListThemesCommand = "verso repl --list-themes"; + private static bool TryResolveKernel(ExtensionHost extensionHost, string value, out string kernelLanguageId, out string error) { var kernels = extensionHost.GetKernels(); @@ -390,9 +402,9 @@ private static bool TryResolveKernel(ExtensionHost extensionHost, string value, { kernelLanguageId = ""; var known = string.Join(", ", kernels.Select(k => k.LanguageId)); - error = $"Error: Kernel '{value}' is not registered." + - (known.Length > 0 ? $" Available kernels: {known}." : "") + - " Run 'verso repl --list-kernels' for details."; + error = Messages.Error(string.Format(Strings.Error_KernelNotRegistered, value)) + + (known.Length > 0 ? " " + string.Format(Strings.Error_AvailableKernels, known) : "") + + " " + string.Format(Strings.Hint_RunForDetails, ListKernelsCommand); return false; } @@ -429,7 +441,7 @@ private static bool TryResolveTheme(ExtensionHost extensionHost, string value, o var ids = string.Join(", ", byName.Select(t => t.ThemeId)); theme = null!; - error = $"Error: Multiple themes share display name '{value}'. Disambiguate by ThemeId: {ids}."; + error = Messages.Error(string.Format(Strings.Error_MultipleThemes, value, ids)); return false; } @@ -444,9 +456,9 @@ private static bool TryResolveTheme(ExtensionHost extensionHost, string value, o theme = null!; var known = string.Join(", ", themes.Select(t => t.DisplayName)); - error = $"Error: Theme '{value}' is not registered." + - (known.Length > 0 ? $" Available themes: {known}." : "") + - " Run 'verso repl --list-themes' for details."; + error = Messages.Error(string.Format(Strings.Error_ThemeNotRegistered, value)) + + (known.Length > 0 ? " " + string.Format(Strings.Error_AvailableThemes, known) : "") + + " " + string.Format(Strings.Hint_RunForDetails, ListThemesCommand); return false; } @@ -457,16 +469,20 @@ private static void PrintKernels(ExtensionHost extensionHost) .ToList(); if (kernels.Count == 0) { - Console.WriteLine("No kernels are registered."); + Console.WriteLine(Strings.List_NoKernels); return; } - var idWidth = Math.Max("LANGUAGE".Length, kernels.Max(k => k.LanguageId.Length)); - var nameWidth = Math.Max("DISPLAY NAME".Length, kernels.Max(k => k.DisplayName.Length)); - Console.WriteLine($"{"LANGUAGE".PadRight(idWidth)} {"DISPLAY NAME".PadRight(nameWidth)} DESCRIPTION"); + // Read once into locals: a column is padded to the width of its own heading, and the + // heading is not the length it was in English. + var languageHeading = Strings.Table_Language; + var displayNameHeading = Strings.Table_DisplayName; + var idWidth = Math.Max(DisplayWidth.Measure(languageHeading), kernels.Max(k => DisplayWidth.Measure(k.LanguageId))); + var nameWidth = Math.Max(DisplayWidth.Measure(displayNameHeading), kernels.Max(k => DisplayWidth.Measure(k.DisplayName))); + Console.WriteLine($"{DisplayWidth.PadRight(languageHeading, idWidth)} {DisplayWidth.PadRight(displayNameHeading, nameWidth)} {Strings.Table_Description}"); foreach (var kernel in kernels) { var description = kernel.Description ?? string.Empty; - Console.WriteLine($"{kernel.LanguageId.PadRight(idWidth)} {kernel.DisplayName.PadRight(nameWidth)} {description}"); + Console.WriteLine($"{DisplayWidth.PadRight(kernel.LanguageId, idWidth)} {DisplayWidth.PadRight(kernel.DisplayName, nameWidth)} {description}"); } } @@ -477,16 +493,18 @@ private static void PrintThemes(ExtensionHost extensionHost) .ToList(); if (themes.Count == 0) { - Console.WriteLine("No themes are registered."); + Console.WriteLine(Strings.List_NoThemes); return; } - var nameWidth = Math.Max("THEME".Length, themes.Max(t => t.DisplayName.Length)); - var kindWidth = Math.Max("KIND".Length, themes.Max(t => t.ThemeKind.ToString().Length)); - Console.WriteLine($"{"THEME".PadRight(nameWidth)} {"KIND".PadRight(kindWidth)} DESCRIPTION"); + var themeHeading = Strings.Table_Theme; + var kindHeading = Strings.Table_Kind; + var nameWidth = Math.Max(DisplayWidth.Measure(themeHeading), themes.Max(t => DisplayWidth.Measure(t.DisplayName))); + var kindWidth = Math.Max(DisplayWidth.Measure(kindHeading), themes.Max(t => t.ThemeKind.ToString().Length)); + Console.WriteLine($"{DisplayWidth.PadRight(themeHeading, nameWidth)} {DisplayWidth.PadRight(kindHeading, kindWidth)} {Strings.Table_Description}"); foreach (var theme in themes) { var description = theme.Description ?? string.Empty; - Console.WriteLine($"{theme.DisplayName.PadRight(nameWidth)} {theme.ThemeKind.ToString().PadRight(kindWidth)} {description}"); + Console.WriteLine($"{DisplayWidth.PadRight(theme.DisplayName, nameWidth)} {theme.ThemeKind.ToString().PadRight(kindWidth)} {description}"); } } } diff --git a/src/Verso.Cli/Commands/RunCommand.cs b/src/Verso.Cli/Commands/RunCommand.cs index 32b7ac8b..d7c8ed73 100644 --- a/src/Verso.Cli/Commands/RunCommand.cs +++ b/src/Verso.Cli/Commands/RunCommand.cs @@ -1,5 +1,6 @@ using System.CommandLine; using Verso.Cli.Execution; +using Verso.Cli.Resources; using Verso.Cli.Utilities; using Verso.Execution; @@ -12,68 +13,52 @@ public static class RunCommand { public static Command Create() { - var notebookArg = new Argument("notebook", "Path to a .verso, .ipynb, or .dib file."); + var notebookArg = new Argument("notebook", Strings.Run_ArgNotebook); - var cellOption = new Option("--cell", "Execute only the specified cell (index or GUID). May be repeated.") + var cellOption = new Option("--cell", Strings.Run_OptCell) { AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore }; - var kernelOption = new Option("--kernel", "Override the notebook's default kernel."); + var kernelOption = new Option("--kernel", Strings.Run_OptKernel); - var outputOption = new Option("--output", () => OutputFormat.Text, - "Output format: text, json, or none."); + var outputOption = new Option("--output", () => OutputFormat.Text, Strings.Run_OptOutput); - var outputFileOption = new Option("--output-file", - "Write output to a file instead of stdout. Implies --output json if no format specified."); + var outputFileOption = new Option("--output-file", Strings.Run_OptOutputFile); - var saveOption = new Option("--save", () => false, - "Save updated outputs back to the notebook file after execution."); + var saveOption = new Option("--save", () => false, Strings.Run_OptSave); - var timeoutOption = new Option("--timeout", () => 300, - "Maximum total execution time in seconds."); + var timeoutOption = new Option("--timeout", () => 300, Strings.Run_OptTimeout); - var extensionsOption = new Option("--extensions", - "Additional directory to scan for extension assemblies."); + var extensionsOption = new Option("--extensions", Strings.Option_Extensions); - var failFastOption = new Option("--fail-fast", () => false, - "Stop execution on the first cell failure."); + var failFastOption = new Option("--fail-fast", () => false, Strings.Run_OptFailFast); - var failOnStderrOption = new Option("--fail-on-stderr", () => false, - "Treat anything a cell writes to standard error as a failure. Off by default, because " + - "progress bars, logging, and warnings are normally written there by programs that are " + - "succeeding. Use this to make a pipeline strict about them."); + var failOnStderrOption = new Option("--fail-on-stderr", () => false, Strings.Run_OptFailOnStderr); - var verboseOption = new Option("--verbose", () => false, - "Print cell execution progress to stderr."); + var verboseOption = new Option("--verbose", () => false, Strings.Run_OptVerbose); - var paramOption = new Option("--param", - "Set a notebook parameter (format: name=value). May be repeated.") + var paramOption = new Option("--param", Strings.Run_OptParam) { AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore }; - var interactiveOption = new Option("--interactive", () => false, - "Prompt for missing required parameters on stdin instead of failing."); + var interactiveOption = new Option("--interactive", () => false, Strings.Run_OptInteractive); - var includeMarkdownOption = new Option("--include-markdown", () => false, - "Include markdown and HTML cell content in terminal output."); + var includeMarkdownOption = new Option("--include-markdown", () => false, Strings.Run_OptIncludeMarkdown); - var showParametersOption = new Option("--show-parameters", () => false, - "Show resolved parameter values in terminal output."); + var showParametersOption = new Option("--show-parameters", () => false, Strings.Run_OptShowParameters); - var trustLocalOption = new Option("--trust-local-assemblies", () => false, - "Allow loading assemblies generated during the current session without consent."); + var trustLocalOption = new Option("--trust-local-assemblies", () => false, Strings.Run_OptTrustLocal); - var ignoreViewStateOption = new Option("--ignore-view-state", () => false, - "Ignore per-cell verso:ui.outputVisibility and verso:ui.inputCollapsed metadata; show all outputs in full."); + var ignoreViewStateOption = new Option("--ignore-view-state", () => false, Strings.Run_OptIgnoreViewState); var pythonOption = PythonInterpreterOption.Create(); var autoInstallOption = PythonAutoInstallOption.Create(); - var command = new Command("run", "Execute a notebook headlessly and stream cell outputs.") + var command = new Command("run", Strings.Run_Description) { notebookArg, cellOption, @@ -132,7 +117,8 @@ public static Command Create() var eqIndex = pv.IndexOf('='); if (eqIndex <= 0) { - Console.Error.WriteLine($"Error: Invalid --param format '{pv}'. Expected name=value."); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Run_InvalidParamFormat, pv))); context.ExitCode = ExitCodes.MissingParameters; return; } @@ -176,7 +162,8 @@ public static Command Create() // Handle file not found if (result.ExitCode == ExitCodes.FileNotFound) { - Console.Error.WriteLine($"Error: Notebook file not found: {notebook.FullName}"); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Error_NotebookNotFound, notebook.FullName))); context.ExitCode = ExitCodes.FileNotFound; return; } @@ -185,7 +172,8 @@ public static Command Create() if (result.ExitCode == ExitCodes.SerializationError) { var ext = Path.GetExtension(notebook.FullName); - Console.Error.WriteLine($"Error: Unsupported or invalid notebook format '{ext}'."); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Run_UnsupportedFormat, ext))); context.ExitCode = ExitCodes.SerializationError; return; } diff --git a/src/Verso.Cli/Commands/ServeCommand.cs b/src/Verso.Cli/Commands/ServeCommand.cs index 7200f1a5..b09ec4af 100644 --- a/src/Verso.Cli/Commands/ServeCommand.cs +++ b/src/Verso.Cli/Commands/ServeCommand.cs @@ -1,5 +1,6 @@ using System.CommandLine; using Verso.Cli.Hosting; +using Verso.Cli.Resources; using Verso.Cli.Utilities; namespace Verso.Cli.Commands; @@ -11,33 +12,26 @@ public static class ServeCommand { public static Command Create() { - var notebookArg = new Argument("notebook", () => null, - "Optional notebook to open on startup.") + var notebookArg = new Argument("notebook", () => null, Strings.Serve_ArgNotebook) { Arity = ArgumentArity.ZeroOrOne }; - var portOption = new Option("--port", () => 5050, - "HTTP port to listen on."); + var portOption = new Option("--port", () => 5050, Strings.Serve_OptPort); - var noBrowserOption = new Option("--no-browser", () => false, - "Do not open a browser tab on startup."); + var noBrowserOption = new Option("--no-browser", () => false, Strings.Serve_OptNoBrowser); - var noHttpsOption = new Option("--no-https", () => false, - "Disable HTTPS (HTTP only)."); + var noHttpsOption = new Option("--no-https", () => false, Strings.Serve_OptNoHttps); - var extensionsOption = new Option("--extensions", - "Directory to scan for additional extension assemblies."); + var extensionsOption = new Option("--extensions", Strings.Option_Extensions); - var verboseOption = new Option("--verbose", () => false, - "Print startup details to stderr."); + var verboseOption = new Option("--verbose", () => false, Strings.Serve_OptVerbose); - var preserveFormatOption = new Option("--preserve-format", () => false, - "When a loaded .ipynb notebook is saved, write back to .ipynb instead of converting to .verso. Cell outputs are preserved."); + var preserveFormatOption = new Option("--preserve-format", () => false, Strings.Serve_OptPreserveFormat); var pythonOption = PythonInterpreterOption.Create(); - var command = new Command("serve", "Launch the Verso Blazor application as a local web server.") + var command = new Command("serve", Strings.Serve_Description) { notebookArg, portOption, @@ -58,6 +52,7 @@ public static Command Create() var extensions = context.ParseResult.GetValueForOption(extensionsOption); var verbose = context.ParseResult.GetValueForOption(verboseOption); var preserveFormat = context.ParseResult.GetValueForOption(preserveFormatOption); + var language = context.ParseResult.GetValueForOption(LanguageOption.Instance); PythonInterpreterOption.Apply(context.ParseResult.GetValueForOption(pythonOption)); @@ -68,7 +63,8 @@ public static Command Create() notebookPath = Path.GetFullPath(notebook.FullName); if (!File.Exists(notebookPath)) { - Console.Error.WriteLine($"Error: Notebook file not found: {notebookPath}"); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Error_NotebookNotFound, notebookPath))); context.ExitCode = ExitCodes.FileNotFound; return; } @@ -82,7 +78,8 @@ public static Command Create() NoHttps = noHttps, Verbose = verbose, ExtensionsDirectory = extensions?.FullName, - PreserveFormat = preserveFormat + PreserveFormat = preserveFormat, + Language = language }; var app = BlazorHostBuilder.Build(options); @@ -93,17 +90,17 @@ public static Command Create() ? $"{baseUrl}/?recover={Uri.EscapeDataString(notebookPath)}" : baseUrl; - Console.WriteLine($"Verso is running at {baseUrl}"); - Console.WriteLine("Press Ctrl+C to stop."); + Console.WriteLine(string.Format(Strings.Serve_Running, baseUrl)); + Console.WriteLine(Strings.Serve_PressCtrlC); if (verbose) { if (!noHttps) Console.Error.WriteLine($" HTTPS: https://localhost:{port + 1}"); if (extensions is not null) - Console.Error.WriteLine($" Extensions: {extensions.FullName}"); + Console.Error.WriteLine(" " + string.Format(Strings.Serve_VerboseExtensions, extensions.FullName)); if (notebookPath is not null) - Console.Error.WriteLine($" Notebook: {notebookPath}"); + Console.Error.WriteLine(" " + string.Format(Strings.Serve_VerboseNotebook, notebookPath)); } // Open the browser after Kestrel has bound its ports to @@ -123,7 +120,8 @@ public static Command Create() } catch (Exception ex) { - Console.Error.WriteLine($"Error: Failed to start server: {ex.Message}"); + Console.Error.WriteLine(Messages.Error( + string.Format(Strings.Serve_StartFailed, ex.Message))); context.ExitCode = ExitCodes.CellFailure; } }); diff --git a/src/Verso.Cli/Execution/HeadlessRunner.cs b/src/Verso.Cli/Execution/HeadlessRunner.cs index bdcfc112..d7dfab48 100644 --- a/src/Verso.Cli/Execution/HeadlessRunner.cs +++ b/src/Verso.Cli/Execution/HeadlessRunner.cs @@ -1,5 +1,6 @@ using Verso.Abstractions; using Verso.Cli.Parameters; +using Verso.Cli.Resources; using Verso.Cli.Utilities; using Verso.Execution; using Verso.Extensions; @@ -43,6 +44,12 @@ public sealed class RunResult /// public sealed class HeadlessRunner { + /// + /// Stands in for a cell's language when it declares none. A language name is an identifier + /// rather than a word, so the absence of one is written the same way in every language. + /// + internal const string UnknownLanguage = "unknown"; + /// /// Executes a notebook with the given options. /// @@ -71,9 +78,8 @@ public async Task ExecuteAsync(RunOptions options, CancellationToken { if (ext.Source == "session-generated local assembly") { - Console.Error.WriteLine( - $"Warning: Refusing session-generated extension '{ext.PackageId}'. " + - "Use --trust-local-assemblies to allow."); + Console.Error.WriteLine(Messages.Warning( + string.Format(Strings.Run_RefusingSessionExtension, ext.PackageId))); return Task.FromResult(false); } } @@ -87,7 +93,8 @@ public async Task ExecuteAsync(RunOptions options, CancellationToken if (options.ExtensionsDirectory is not null) { - Console.Error.WriteLine($"Warning: Loading third-party extensions from '{options.ExtensionsDirectory}'. These extensions are auto-approved for headless execution."); + Console.Error.WriteLine(Messages.Warning( + string.Format(Strings.Run_LoadingThirdParty, options.ExtensionsDirectory))); await extensionHost.LoadFromDirectoryAsync(options.ExtensionsDirectory); } @@ -185,10 +192,10 @@ public async Task ExecuteAsync(RunOptions options, CancellationToken ct.ThrowIfCancellationRequested(); var cellId = cellsToExecute[i]; var cell = notebook.Cells.FirstOrDefault(c => c.Id == cellId); - var lang = cell?.Language ?? "unknown"; + var lang = cell?.Language ?? UnknownLanguage; if (options.Verbose) - Console.Error.WriteLine($"[{i}/{total}] Executing cell {i} ({lang})..."); + Console.Error.WriteLine(string.Format(Strings.Run_ExecutingCell, i, total, lang)); var cellSw = System.Diagnostics.Stopwatch.StartNew(); var result = await scaffold.ExecuteCellAsync(cellId, ct); @@ -196,7 +203,8 @@ public async Task ExecuteAsync(RunOptions options, CancellationToken results.Add(result); if (options.Verbose) - Console.Error.WriteLine($"[{i}/{total}] Cell {i} completed in {cellSw.Elapsed.TotalSeconds:F1}s ({result.Status})"); + Console.Error.WriteLine(string.Format( + Strings.Run_CellCompleted, i, total, cellSw.Elapsed.TotalSeconds.ToString("F1"), result.Status)); if (options.FailFast && CellHasErrors(cellId, notebook, result)) break; @@ -212,7 +220,8 @@ public async Task ExecuteAsync(RunOptions options, CancellationToken var cell = notebook.Cells[i]; if (options.Verbose) - Console.Error.WriteLine($"[{i}/{total}] Executing cell {i} ({cell.Language ?? "unknown"})..."); + Console.Error.WriteLine(string.Format( + Strings.Run_ExecutingCell, i, total, cell.Language ?? UnknownLanguage)); var cellSw = System.Diagnostics.Stopwatch.StartNew(); var result = await scaffold.ExecuteCellAsync(cell.Id, ct); @@ -220,7 +229,8 @@ public async Task ExecuteAsync(RunOptions options, CancellationToken results.Add(result); if (options.Verbose) - Console.Error.WriteLine($"[{i}/{total}] Cell {i} completed in {cellSw.Elapsed.TotalSeconds:F1}s ({result.Status})"); + Console.Error.WriteLine(string.Format( + Strings.Run_CellCompleted, i, total, cellSw.Elapsed.TotalSeconds.ToString("F1"), result.Status)); if (CellHasErrors(cell.Id, notebook, result)) break; @@ -238,14 +248,16 @@ public async Task ExecuteAsync(RunOptions options, CancellationToken ct.ThrowIfCancellationRequested(); var cell = notebook.Cells[i]; - Console.Error.WriteLine($"[{i}/{total}] Executing cell {i} ({cell.Language ?? "unknown"})..."); + Console.Error.WriteLine(string.Format( + Strings.Run_ExecutingCell, i, total, cell.Language ?? UnknownLanguage)); var cellSw = System.Diagnostics.Stopwatch.StartNew(); var result = await scaffold.ExecuteCellAsync(cell.Id, ct); cellSw.Stop(); results.Add(result); - Console.Error.WriteLine($"[{i}/{total}] Cell {i} completed in {cellSw.Elapsed.TotalSeconds:F1}s ({result.Status})"); + Console.Error.WriteLine(string.Format( + Strings.Run_CellCompleted, i, total, cellSw.Elapsed.TotalSeconds.ToString("F1"), result.Status)); } } else @@ -333,7 +345,7 @@ public async Task ExecuteAsync(RunOptions options, CancellationToken } else { - throw new ArgumentException($"Invalid cell selector '{selector}'. Use a 0-based index or a cell GUID."); + throw new ArgumentException(string.Format(Strings.Run_InvalidCellSelector, selector)); } } return resolved; diff --git a/src/Verso.Cli/Execution/JsonOutputWriter.cs b/src/Verso.Cli/Execution/JsonOutputWriter.cs index fef28e7f..67cb5193 100644 --- a/src/Verso.Cli/Execution/JsonOutputWriter.cs +++ b/src/Verso.Cli/Execution/JsonOutputWriter.cs @@ -110,6 +110,8 @@ public static JsonOutputDocument Build( } catch (Exception ex) when (ex is not OperationCanceledException) { + // English, like the statuses below: this stands in for a value inside a machine-read + // document, next to a type name that is not translated either. return $"<{value.GetType().Name}, which has no JSON form>"; } } @@ -144,6 +146,12 @@ public static async Task WriteToFileAsync(JsonOutputDocument document, string fi /// The reported status of a cell: what was recorded, unless the cell produced an error output /// while completing, which is how a raised exception usually arrives. ///
+ /// + /// The values here stay in English. This document is what --output json writes, and it + /// is read by whatever the run was piped into rather than by a person; the other statuses are + /// enum names, so a translated one would be the only value in the document a caller could not + /// compare against. + /// private static string StatusOf(CellModel cell, ExecutionResult? result) { if (result is null) diff --git a/src/Verso.Cli/Execution/OutputRenderer.cs b/src/Verso.Cli/Execution/OutputRenderer.cs index 07d1d7f3..204d8879 100644 --- a/src/Verso.Cli/Execution/OutputRenderer.cs +++ b/src/Verso.Cli/Execution/OutputRenderer.cs @@ -1,5 +1,7 @@ using System.Text.RegularExpressions; using Verso.Abstractions; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; using Verso.Execution; using Verso.Extensions.Utilities; @@ -56,8 +58,8 @@ public void RenderCell(int index, CellModel cell, ExecutionResult result, if (cell.Type is "code") { - var language = cell.Language ?? "unknown"; - _stdout.WriteLine($"\u2500\u2500\u2500 Cell {index} ({language}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"); + var language = cell.Language ?? HeadlessRunner.UnknownLanguage; + WriteRule($"{string.Format(Strings.Render_CellLabel, index)} ({language})"); if (!hideOutputs) { @@ -71,26 +73,26 @@ public void RenderCell(int index, CellModel cell, ExecutionResult result, } else if (_showParameters && cell.Type is "parameters") { - _stdout.WriteLine($"\u2500\u2500\u2500 Cell {index} (parameters) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"); + WriteRule($"{string.Format(Strings.Render_CellLabel, index)} ({cell.Type})"); if (resolvedParameters is { Count: > 0 }) { - var maxKey = resolvedParameters.Keys.Max(k => k.Length); + var maxKey = resolvedParameters.Keys.Max(DisplayWidth.Measure); foreach (var (name, value) in resolvedParameters) { - _stdout.WriteLine($" {name.PadRight(maxKey)} {value}"); + _stdout.WriteLine($" {DisplayWidth.PadRight(name, maxKey)} {value}"); } } else { - _stdout.WriteLine(" (no parameters)"); + _stdout.WriteLine(" " + Strings.Render_NoParameters); } _stdout.WriteLine(); } else if (_includeMarkdown && cell.Type is "markdown") { - _stdout.WriteLine($"\u2500\u2500\u2500 Cell {index} (markdown) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"); + WriteRule($"{string.Format(Strings.Render_CellLabel, index)} ({cell.Type})"); if (!inputCollapsed && !string.IsNullOrWhiteSpace(cell.Source)) _stdout.WriteLine(cell.Source); @@ -99,7 +101,7 @@ public void RenderCell(int index, CellModel cell, ExecutionResult result, } else if (_includeMarkdown && cell.Type is "html") { - _stdout.WriteLine($"\u2500\u2500\u2500 Cell {index} (html) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"); + WriteRule($"{string.Format(Strings.Render_CellLabel, index)} ({cell.Type})"); if (!inputCollapsed) { @@ -135,9 +137,24 @@ public void WriteSummary( var failed = results.Count(r => CellOutcome.Failed(CellOutcome.Find(cells, r.CellId), r)); var total = results.Count; - _stdout.WriteLine($"\u2500\u2500\u2500 Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"); - _stdout.WriteLine($"Cells: {total} total, {succeeded} succeeded, {failed} failed"); - _stdout.WriteLine($"Time: {totalElapsed.TotalSeconds:F1}s"); + WriteRule(Strings.Render_SummaryLabel); + _stdout.WriteLine(string.Format(Strings.Render_SummaryCells, total, succeeded, failed)); + _stdout.WriteLine(string.Format(Strings.Render_SummaryTime, totalElapsed.TotalSeconds.ToString("F1"))); + } + + /// + /// Writes the rule that heads a block of output. + /// + /// + /// The trailing rule is drawn to fill whatever the label left, rather than written out as a + /// fixed run of dashes per kind of cell. A translated label is not the length the English one + /// was, and headings that no longer line up read as a rendering fault. + /// + private void WriteRule(string label) + { + const int Width = 42; + var head = $"\u2500\u2500\u2500 {label} "; + _stdout.WriteLine(head + new string('\u2500', Math.Max(3, Width - DisplayWidth.Measure(head)))); } private void RenderOutput(CellOutput output, bool preview, int previewLineCount) @@ -220,7 +237,8 @@ private void WriteMaybeTruncated(string content, bool preview, int previewLineCo _stdout.WriteLine(lines[i]); var omitted = lines.Length - previewLineCount; - _stdout.WriteLine($"... ({omitted} more {(omitted == 1 ? "line" : "lines")})"); + _stdout.WriteLine(string.Format( + Plural.Of(omitted, Strings.Render_MoreLines_One, Strings.Render_MoreLines_Other), omitted)); } /// @@ -229,6 +247,13 @@ private void WriteMaybeTruncated(string content, bool preview, int previewLineCo /// because it did not fail anything. The tag is still textual rather than colour alone, so the /// distinction survives being piped to a file. /// + /// + /// The two tags below stay in English. They are the one part of a run's output a script reads + /// rather than a person: piping a run through something that looks for them is the reason + /// they are written at all, and a build that answered in a different language on a different + /// machine would break every one of those pipelines. Everything after the tag is the cell's + /// own words, and those were never Verso's to translate. + /// private void WriteStandardError(string content) { if (_supportsAnsi) diff --git a/src/Verso.Cli/Execution/ToolbarActionResolver.cs b/src/Verso.Cli/Execution/ToolbarActionResolver.cs index 88111af6..bf899ce6 100644 --- a/src/Verso.Cli/Execution/ToolbarActionResolver.cs +++ b/src/Verso.Cli/Execution/ToolbarActionResolver.cs @@ -1,4 +1,6 @@ using Verso.Abstractions; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; using Verso.Extensions; namespace Verso.Cli.Execution; @@ -47,7 +49,7 @@ public static bool TryResolveAction( var ids = string.Join(", ", byName.Select(a => a.ActionId)); action = null!; - error = $"Error: Multiple export actions share display name '{format}'. Disambiguate by ActionId: {ids}."; + error = Messages.Error(string.Format(Strings.Error_MultipleExportActions, format, ids)); return false; } @@ -60,7 +62,7 @@ public static bool TryResolveAction( } action = null!; - error = $"Error: Export format '{format}' is not registered."; + error = Messages.Error(string.Format(Strings.Error_ExportFormatNotRegistered, format)); return false; } @@ -100,7 +102,7 @@ public static bool TryResolveTheme( var ids = string.Join(", ", byName.Select(t => t.ThemeId)); theme = null!; - error = $"Error: Multiple themes share display name '{value}'. Disambiguate by ThemeId: {ids}."; + error = Messages.Error(string.Format(Strings.Error_MultipleThemes, value, ids)); return false; } @@ -115,8 +117,8 @@ public static bool TryResolveTheme( theme = null!; var known = string.Join(", ", themes.Select(t => t.DisplayName)); - error = $"Error: Theme '{value}' is not registered." + - (known.Length > 0 ? $" Available themes: {known}." : ""); + error = Messages.Error(string.Format(Strings.Error_ThemeNotRegistered, value)) + + (known.Length > 0 ? " " + string.Format(Strings.Error_AvailableThemes, known) : ""); return false; } } diff --git a/src/Verso.Cli/Hosting/BlazorHostBuilder.cs b/src/Verso.Cli/Hosting/BlazorHostBuilder.cs index 2b9a2a13..347b0e34 100644 --- a/src/Verso.Cli/Hosting/BlazorHostBuilder.cs +++ b/src/Verso.Cli/Hosting/BlazorHostBuilder.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Verso.Blazor.Localization; using Verso.Blazor.Services; using Verso.Blazor.Shared.Services; using Verso.Extensions; @@ -21,6 +22,11 @@ public sealed record ServeOptions public bool Verbose { get; init; } public string? ExtensionsDirectory { get; init; } public bool PreserveFormat { get; init; } + + /// + /// Interface language to serve, or null to let each browser negotiate one. + /// + public string? Language { get; init; } } /// @@ -113,6 +119,10 @@ public static WebApplication Build(ServeOptions options) if (!options.NoHttps) app.UseHttpsRedirection(); + // Before the components are mapped: a circuit takes its culture from the request that + // opened it. Shared with Verso.Blazor/Program.cs so the two hosts cannot drift apart. + app.UseVersoLocalization(options.Language); + app.UseStaticFiles(); app.UseAntiforgery(); diff --git a/src/Verso.Cli/Parameters/ParameterResolver.cs b/src/Verso.Cli/Parameters/ParameterResolver.cs index 06b16a84..702a57f8 100644 --- a/src/Verso.Cli/Parameters/ParameterResolver.cs +++ b/src/Verso.Cli/Parameters/ParameterResolver.cs @@ -1,4 +1,6 @@ using Verso.Abstractions; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Parameters; @@ -75,7 +77,8 @@ public ParameterResolutionResult Resolve() else { // Unknown parameter: inject as string with warning - _error.WriteLine($"Warning: Unknown parameter '{name}' not defined in notebook metadata. Injecting as string."); + _error.WriteLine(Messages.Warning( + string.Format(Strings.Param_UnknownParameter, name))); resolved[name] = rawValue; } } @@ -84,7 +87,7 @@ public ParameterResolutionResult Resolve() if (errors.Count > 0) { return ParameterResolutionResult.Failure( - "Error: Invalid parameter values:\n" + string.Join("\n", errors)); + Messages.Error(Strings.Param_InvalidValues) + "\n" + string.Join("\n", errors)); } // 2. Apply defaults for unspecified parameters @@ -122,9 +125,9 @@ public ParameterResolutionResult Resolve() }); return ParameterResolutionResult.Failure( - "Error: Missing required notebook parameters:\n\n" + + Messages.Error(Strings.Param_MissingRequired) + "\n\n" + string.Join("\n", lines) + - "\n\nSupply values with --param or use --interactive to be prompted."); + "\n\n" + Strings.Param_SupplyHint); } // 5. Sort by Order then alphabetically @@ -139,7 +142,7 @@ private ParameterResolutionResult ResolveUntyped() return ParameterResolutionResult.Success(new Dictionary()); } - _error.WriteLine("Warning: Parameter definitions are only supported for .verso files. Injecting all --param values as untyped strings."); + _error.WriteLine(Messages.Warning(Strings.Param_UntypedOnly)); var resolved = new Dictionary(); foreach (var (name, value) in _cliParams) @@ -159,7 +162,7 @@ private void PromptForParameters( .ToList(); _output.WriteLine(); - _output.WriteLine("Notebook parameters:"); + _output.WriteLine(Strings.Param_Heading); _output.WriteLine(); foreach (var (name, def) in sortedDefs) @@ -167,8 +170,10 @@ private void PromptForParameters( // Skip parameters already resolved via CLI if (resolved.ContainsKey(name)) continue; - var typeLabel = def.Required ? $"{def.Type}, required" : def.Type; - var defaultLabel = def.Default is not null ? $", default: {def.Default}" : ""; + var typeLabel = def.Required ? $"{def.Type}, {Strings.Param_Required}" : def.Type; + var defaultLabel = def.Default is not null + ? ", " + string.Format(Strings.Param_Default, def.Default) + : ""; var desc = def.Description is not null ? $" {def.Description}" : ""; _output.WriteLine($" {name} ({typeLabel}{defaultLabel}){desc}"); @@ -191,7 +196,7 @@ private void PromptForParameters( { break; // Optional with no default, skip } - _output.WriteLine(" Value is required."); + _output.WriteLine(" " + Strings.Param_ValueRequired); continue; } diff --git a/src/Verso.Cli/Program.cs b/src/Verso.Cli/Program.cs index 4d052d19..beb73285 100644 --- a/src/Verso.Cli/Program.cs +++ b/src/Verso.Cli/Program.cs @@ -2,9 +2,17 @@ using System.CommandLine.Builder; using System.CommandLine.Parsing; using Verso.Cli.Commands; +using Verso.Cli.Resources; using Verso.Cli.Utilities; -var rootCommand = new RootCommand("Verso CLI — execute, serve, and convert Verso notebooks."); +// Ahead of the command tree, because building it reads every description. +LanguageOption.ApplyFromArguments(args); + +var rootCommand = new RootCommand(Strings.Root_Description); + +// Global rather than per-command: it has to be accepted before a subcommand name so that +// "verso --language de --help" works, and every subcommand reads the same instance. +rootCommand.AddGlobalOption(LanguageOption.Instance); // Subcommands rootCommand.AddCommand(RunCommand.Create()); @@ -20,7 +28,7 @@ .UseVersionOption() .UseExceptionHandler((ex, context) => { - Console.Error.WriteLine($"Unhandled error: {ex.Message}"); + Console.Error.WriteLine(string.Format(Strings.Root_UnhandledError, ex.Message)); context.ExitCode = ExitCodes.CellFailure; }) .Build(); diff --git a/src/Verso.Cli/README.md b/src/Verso.Cli/README.md index fb4f9852..cb27d8a1 100644 --- a/src/Verso.Cli/README.md +++ b/src/Verso.Cli/README.md @@ -17,6 +17,14 @@ dotnet tool update -g Verso.Cli Requires .NET 8.0 SDK or later. +## Global Options + +| Option | Default | Description | +|--------|---------|-------------| +| `--language ` | system | Interface language: `en`, `de`, `es`, `ja`, or `zh-Hans` | + +Accepted by every command, and before the command name as well, so `verso --language de --help` prints the help in German. Without it the language comes from the `VERSO_LANGUAGE` environment variable, then from the operating system, then English. Only the words change: numbers and dates keep the machine's own formatting, and the `[error]` tags and `--output json` status values stay in English so a pipeline that reads them cannot break. + ## Commands ### `verso serve` diff --git a/src/Verso.Cli/Repl/Meta/Commands/ClearMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/ClearMeta.cs index 0e6be34c..5e733269 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/ClearMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/ClearMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -6,11 +8,10 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class ClearMeta : IMetaCommand { public string Name => "clear"; - public string Summary => "Clears the terminal."; - public string DetailedHelp => - ".clear\n" + - " Clears the terminal screen. Session state (kernel variables, notebook cells)\n" + - " is preserved; only the scrollback is cleared."; + public string Summary => Strings.Meta_Clear_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".clear\n" + Strings.Meta_Clear_Details; public Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { diff --git a/src/Verso.Cli/Repl/Meta/Commands/ConvertMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/ConvertMeta.cs index 6457e588..20a36c35 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/ConvertMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/ConvertMeta.cs @@ -1,5 +1,6 @@ using Spectre.Console; using Verso.Abstractions; +using Verso.Cli.Resources; using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -11,19 +12,17 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class ConvertMeta : IMetaCommand { public string Name => "convert"; - public string Summary => "Writes the session notebook to using the serializer matching its extension."; - public string DetailedHelp => - ".convert \n" + - " Serializes the current session notebook to using the serializer whose\n" + - " FileExtensions include the target extension. Does not change the session's\n" + - " loaded path. Identical resolution to 'verso convert'."; + public string Summary => Strings.Meta_Convert_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".convert \n" + Strings.Meta_Convert_Details; public async Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { var arg = argumentText.Trim(); if (string.IsNullOrEmpty(arg)) { - context.Console.MarkupLine("[red]Usage: .convert [/]"); + context.Console.MarkupLine(Messages.In("red", Messages.Typed(Strings.Repl_Usage, ".convert "))); return true; } @@ -36,7 +35,7 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C } catch (SerializerNotFoundException ex) { - context.Console.MarkupLine($"[red]{Markup.Escape(ex.Message)}[/]"); + context.Console.MarkupLine(Messages.In("red", Markup.Escape(ex.Message))); return true; } @@ -48,11 +47,13 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C Directory.CreateDirectory(directory); await File.WriteAllTextAsync(targetPath, content, ct); - context.Console.MarkupLine($"[green]Converted[/] session notebook → {Markup.Escape(targetPath)} ({context.Session.Notebook.Cells.Count} cells)"); + context.Console.MarkupLine(Messages.In("green", Messages.Say( + Strings.Meta_Convert_Done, targetPath, CellCount.Describe(context.Session.Notebook.Cells.Count)))); } catch (Exception ex) { - context.Console.MarkupLine($"[red]Convert failed: {Markup.Escape(ex.Message)}[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Say(Strings.Meta_Convert_Failed, ex.Message))); } return true; diff --git a/src/Verso.Cli/Repl/Meta/Commands/ExitMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/ExitMeta.cs index b4d3e1f0..c4777131 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/ExitMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/ExitMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -7,17 +9,18 @@ public sealed class ExitMeta : IMetaCommand { public string Name => "exit"; public IReadOnlyList Aliases => new[] { "quit" }; - public string Summary => "Exits the REPL."; - public string DetailedHelp => - ".exit / .quit\n" + - " Exits the REPL. When unsaved cells exist, prompts for confirmation\n" + - " unless confirmOnExit is disabled in user settings."; + public string Summary => Strings.Meta_Exit_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".exit / .quit\n" + Strings.Meta_Exit_Details; public Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { if (context.Session.Settings.ConfirmOnExit && !context.Session.ConfirmDiscardUnsavedChanges()) { - context.Console.MarkupLine("[yellow]Session has unsaved cells.[/] Type [bold].save[/] first, or [bold].exit[/] again to discard."); + context.Console.MarkupLine( + Messages.In("yellow", Messages.Say(Strings.Repl_UnsavedCells)) + + " " + Messages.Typed(Strings.Repl_UnsavedHint, ".save", ".exit")); return Task.FromResult(true); } return Task.FromResult(false); diff --git a/src/Verso.Cli/Repl/Meta/Commands/ExportMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/ExportMeta.cs index fb8e90a4..558c015b 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/ExportMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/ExportMeta.cs @@ -1,6 +1,8 @@ using Spectre.Console; using Verso.Abstractions; using Verso.Cli.Execution; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; using Verso.Contexts; namespace Verso.Cli.Repl.Meta.Commands; @@ -13,32 +15,37 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class ExportMeta : IMetaCommand { public string Name => "export"; - public string Summary => "Exports the session notebook via an ExportMenu toolbar action."; + public string Summary => Strings.Meta_Export_Summary; + + // The first line is what the reader types, so it is written here rather than translated. public string DetailedHelp => - ".export --format [--output ] [--layout ] [--theme ]\n" + - " Dispatches to an IToolbarAction registered with ToolbarPlacement.ExportMenu.\n" + - " Format is matched by DisplayName (case-insensitive), ActionId as fallback.\n" + - " Theme is matched by DisplayName (case-insensitive), ThemeId as fallback.\n" + - " Without --output, writes the action's suggested filename to the current directory.\n" + - " Identical to 'verso export'."; + ".export --format [--output ] [--layout ] [--theme ]\n" + + Strings.Meta_Export_Details; + + /// What to type to see the formats installed. The same in every language. + private const string ListExportersCommand = ".list exporters"; public async Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { if (!TryParseArgs(argumentText, out var format, out var outputPath, out var layoutId, out var themeName, out var parseError)) { - context.Console.MarkupLine($"[red]{Markup.Escape(parseError)}[/]"); + context.Console.MarkupLine(Messages.In("red", Markup.Escape(parseError))); return true; } if (string.IsNullOrEmpty(format)) { - context.Console.MarkupLine("[red]--format is required.[/] Run [bold].list exporters[/] to see available formats."); + context.Console.MarkupLine( + Messages.In("red", Messages.Say(Strings.Export_FormatRequired)) + + " " + Messages.Typed(Strings.Hint_RunForFormats, ListExportersCommand)); return true; } if (!ToolbarActionResolver.TryResolveAction(context.Session.ExtensionHost, format, out var action, out var actionError)) { - context.Console.MarkupLine($"[red]{Markup.Escape(actionError)}[/] Run [bold].list exporters[/] to see available formats."); + context.Console.MarkupLine( + Messages.In("red", Markup.Escape(actionError)) + + " " + Messages.Typed(Strings.Hint_RunForFormats, ListExportersCommand)); return true; } @@ -47,7 +54,7 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C { if (!ToolbarActionResolver.TryResolveTheme(context.Session.ExtensionHost, themeName, out selectedTheme, out var themeError)) { - context.Console.MarkupLine($"[red]{Markup.Escape(themeError)}[/]"); + context.Console.MarkupLine(Messages.In("red", Markup.Escape(themeError))); return true; } } @@ -70,17 +77,20 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C } catch (Exception ex) { - context.Console.MarkupLine($"[red]Export action '{Markup.Escape(action.ActionId)}' threw: {Markup.Escape(ex.Message)}[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Say(Strings.Export_ActionThrew, action.ActionId, ex.Message))); return true; } if (ctx.WrittenPath is null) { - context.Console.MarkupLine($"[red]Export action '{Markup.Escape(action.ActionId)}' did not produce a file.[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Say(Strings.Export_NoFileProduced, action.ActionId))); return true; } - context.Console.MarkupLine($"[green]Exported[/] session notebook → {Markup.Escape(ctx.WrittenPath)} ({context.Session.Notebook.Cells.Count} cells)"); + context.Console.MarkupLine(Messages.In("green", Messages.Say( + Strings.Meta_Export_Done, ctx.WrittenPath, CellCount.Describe(context.Session.Notebook.Cells.Count)))); return true; } @@ -105,24 +115,24 @@ private static bool TryParseArgs( { case "--format": case "-f": - if (i + 1 >= tokens.Count) { error = "Missing value for --format."; return false; } + if (i + 1 >= tokens.Count) { error = string.Format(Strings.Meta_Export_MissingValue, "--format"); return false; } format = tokens[++i]; break; case "--output": case "-o": - if (i + 1 >= tokens.Count) { error = "Missing value for --output."; return false; } + if (i + 1 >= tokens.Count) { error = string.Format(Strings.Meta_Export_MissingValue, "--output"); return false; } outputPath = Path.GetFullPath(tokens[++i]); break; case "--layout": - if (i + 1 >= tokens.Count) { error = "Missing value for --layout."; return false; } + if (i + 1 >= tokens.Count) { error = string.Format(Strings.Meta_Export_MissingValue, "--layout"); return false; } layoutId = tokens[++i]; break; case "--theme": - if (i + 1 >= tokens.Count) { error = "Missing value for --theme."; return false; } + if (i + 1 >= tokens.Count) { error = string.Format(Strings.Meta_Export_MissingValue, "--theme"); return false; } themeName = tokens[++i]; break; default: - error = $"Unknown argument: {tokens[i]}"; + error = string.Format(Strings.Meta_Export_UnknownArgument, tokens[i]); return false; } } diff --git a/src/Verso.Cli/Repl/Meta/Commands/HelpMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/HelpMeta.cs index b6ba404c..3920a119 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/HelpMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/HelpMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -6,11 +8,10 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class HelpMeta : IMetaCommand { public string Name => "help"; - public string Summary => "Prints meta-command help."; - public string DetailedHelp => - ".help []\n" + - " With no argument, prints an overview of all meta-commands.\n" + - " With a name, prints detailed help for that command."; + public string Summary => Strings.Meta_Help_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".help []\n" + Strings.Meta_Help_Details; public Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { @@ -23,7 +24,9 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell } else { - context.Console.MarkupLine($"[red]Unknown meta-command '.{trimmed}'.[/] Type [bold].help[/] for the list."); + context.Console.MarkupLine( + Messages.In("red", Messages.Say(Strings.Repl_UnknownMetaCommand, trimmed)) + + " " + Messages.Typed(Strings.Repl_TypeHelpForList, ".help")); } return Task.FromResult(true); } @@ -31,17 +34,22 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell if (context.UseColor) { var table = new Table().Border(TableBorder.Rounded); - table.AddColumn("Command"); - table.AddColumn("Description"); + table.AddColumn(Strings.Table_Command); + table.AddColumn(Strings.Table_Description); foreach (var command in context.Registry.AllOrdered) table.AddRow($"[bold].{command.Name}[/]", Markup.Escape(command.Summary)); context.Console.Write(table); } else { - Console.Out.WriteLine($"{"COMMAND",-12} DESCRIPTION"); + // Read once into a local: a column is padded to the width of its own heading, + // and the heading is not the length it was in English. + var commandHeading = Strings.Table_Command; + var width = Math.Max(DisplayWidth.Measure(commandHeading), + context.Registry.AllOrdered.Max(c => c.Name.Length + 1)); + Console.Out.WriteLine($"{DisplayWidth.PadRight(commandHeading, width)} {Strings.Table_Description}"); foreach (var command in context.Registry.AllOrdered) - Console.Out.WriteLine($"{"." + command.Name,-12} {command.Summary}"); + Console.Out.WriteLine($"{DisplayWidth.PadRight("." + command.Name, width)} {command.Summary}"); } return Task.FromResult(true); } diff --git a/src/Verso.Cli/Repl/Meta/Commands/HistoryMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/HistoryMeta.cs index 0d3e5c02..e4c548ac 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/HistoryMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/HistoryMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -6,11 +8,10 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class HistoryMeta : IMetaCommand { public string Name => "history"; - public string Summary => "Prints recent cell submissions."; - public string DetailedHelp => - ".history []\n" + - " Prints the last n submitted cells (default 20). Each entry shows the input counter\n" + - " and a preview of the first non-empty line of source."; + public string Summary => Strings.Meta_History_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".history []\n" + Strings.Meta_History_Details; public Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { @@ -20,7 +21,9 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell { if (!int.TryParse(arg, out n) || n <= 0) { - context.Console.MarkupLine($"[red]Invalid count '{Markup.Escape(arg)}'.[/] Usage: .history []"); + context.Console.MarkupLine( + Messages.In("red", Messages.Say(Strings.Meta_History_InvalidCount, arg)) + + " " + Messages.Typed(Strings.Repl_Usage, ".history []")); return Task.FromResult(true); } } @@ -30,7 +33,7 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell if (cells.Count == 0) { - context.Console.MarkupLine("[dim]No history.[/]"); + context.Console.MarkupLine(Messages.In("dim", Messages.Say(Strings.Meta_History_Empty))); return Task.FromResult(true); } diff --git a/src/Verso.Cli/Repl/Meta/Commands/KernelMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/KernelMeta.cs index d058671e..ca5a5e0c 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/KernelMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/KernelMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -6,21 +8,18 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class KernelMeta : IMetaCommand { public string Name => "kernel"; - public string Summary => "Prints or switches the active kernel."; - public string DetailedHelp => - ".kernel []\n" + - " With no argument, prints the active kernel.\n" + - " With an id (LanguageId, matched case-insensitively), switches the active kernel\n" + - " for subsequent cells. Variables already declared in the prior kernel remain in\n" + - " that kernel's scope."; + public string Summary => Strings.Meta_Kernel_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".kernel []\n" + Strings.Meta_Kernel_Details; public async Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { var arg = argumentText.Trim(); if (string.IsNullOrEmpty(arg)) { - var current = context.Session.ActiveKernelId ?? ""; - context.Console.MarkupLine($"Active kernel: [bold]{Markup.Escape(current)}[/]"); + var current = context.Session.ActiveKernelId ?? Strings.Repl_MarkerNone; + context.Console.MarkupLine(Messages.Typed(Strings.Meta_Kernel_Active, current)); return true; } @@ -31,12 +30,15 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C if (match is null) { var known = string.Join(", ", kernels.Select(k => k.LanguageId)); - context.Console.MarkupLine($"[red]Kernel '{Markup.Escape(arg)}' is not registered.[/] Available: {Markup.Escape(known)}"); + context.Console.MarkupLine( + Messages.In("red", Messages.Say(Strings.Error_KernelNotRegistered, arg)) + + " " + Messages.Say(Strings.Error_AvailableKernels, known)); return true; } context.Session.ActiveKernelId = match.LanguageId; - context.Console.MarkupLine($"Switched to kernel: [bold]{Markup.Escape(match.LanguageId)}[/] ({Markup.Escape(match.DisplayName)})"); + context.Console.MarkupLine(Messages.Say( + Strings.Meta_Kernel_Switched, match.LanguageId, match.DisplayName)); // Eagerly initialize the kernel so the first keystroke doesn't hit a // cold-start on the completion path. Without this, completion for CSharp @@ -48,7 +50,8 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C } catch (Exception ex) { - context.Console.MarkupLine($"[yellow]Warning: kernel warm-up failed: {Markup.Escape(ex.Message)}[/]"); + context.Console.MarkupLine(Messages.In("yellow", + Messages.Warning(Messages.Say(Strings.Meta_Kernel_WarmUpFailed, ex.Message)))); } return true; diff --git a/src/Verso.Cli/Repl/Meta/Commands/LayoutMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/LayoutMeta.cs index 7f873431..66f7548c 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/LayoutMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/LayoutMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -6,32 +8,30 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class LayoutMeta : IMetaCommand { public string Name => "layout"; - public string Summary => "Prints or sets the default export layout."; - public string DetailedHelp => - ".layout [|none]\n" + - " With no argument, prints the active layout id.\n" + - " With an id, sets the default ActiveLayoutId for subsequent .export calls.\n" + - " Pass 'none' to clear the layout."; + public string Summary => Strings.Meta_Layout_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".layout [|none]\n" + Strings.Meta_Layout_Details; public Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { var arg = argumentText.Trim(); if (string.IsNullOrEmpty(arg)) { - var current = context.Session.ActiveLayoutId ?? ""; - context.Console.MarkupLine($"Active layout: [bold]{Markup.Escape(current)}[/]"); + var current = context.Session.ActiveLayoutId ?? Strings.Repl_MarkerNone; + context.Console.MarkupLine(Messages.Typed(Strings.Meta_Layout_Active, current)); return Task.FromResult(true); } if (string.Equals(arg, "none", StringComparison.OrdinalIgnoreCase)) { context.Session.ActiveLayoutId = null; - context.Console.MarkupLine("[dim]Layout cleared.[/]"); + context.Console.MarkupLine(Messages.In("dim", Messages.Say(Strings.Meta_Layout_Cleared))); return Task.FromResult(true); } context.Session.ActiveLayoutId = arg; - context.Console.MarkupLine($"Layout set to: [bold]{Markup.Escape(arg)}[/]"); + context.Console.MarkupLine(Messages.Typed(Strings.Meta_Layout_Set, arg)); return Task.FromResult(true); } } diff --git a/src/Verso.Cli/Repl/Meta/Commands/ListMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/ListMeta.cs index 0fab6c5a..8f24550a 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/ListMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/ListMeta.cs @@ -1,5 +1,7 @@ using Spectre.Console; using Verso.Abstractions; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -7,19 +9,21 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class ListMeta : IMetaCommand { public string Name => "list"; - public string Summary => "Lists registered extension capabilities."; - public string DetailedHelp => - ".list \n" + - " Where is one of:\n" + - " kernels, themes, formatters, renderers, serializers, extensions, exporters\n" + - " Prints a Spectre-styled table of the registered items for that capability."; + public string Summary => Strings.Meta_List_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".list \n" + Strings.Meta_List_Details; + + /// What .list accepts. Typed at a keyboard, so the same in every language. + private const string Kinds = "kernels, themes, formatters, renderers, serializers, extensions, exporters"; public Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { var kind = argumentText.Trim().ToLowerInvariant(); if (string.IsNullOrEmpty(kind)) { - context.Console.MarkupLine("[yellow]Usage:[/] .list "); + context.Console.MarkupLine(Messages.In("yellow", + Messages.Typed(Strings.Repl_Usage, ".list <" + Kinds.Replace(", ", "|") + ">"))); return Task.FromResult(true); } @@ -27,37 +31,37 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell switch (kind) { case "kernels": - RenderTable(context, new[] { "Language", "Description" }, + RenderTable(context, new[] { Strings.Table_Language, Strings.Table_Description }, host.GetKernels().Select(k => new[] { k.LanguageId, k.Description ?? "" })); break; case "themes": - RenderTable(context, new[] { "Theme", "Kind", "Description" }, + RenderTable(context, new[] { Strings.Table_Theme, Strings.Table_Kind, Strings.Table_Description }, host.GetThemes().Select(t => new[] { t.DisplayName, t.ThemeKind.ToString(), t.Description ?? "" })); break; case "formatters": - RenderTable(context, new[] { "Name", "Description", "Priority" }, + RenderTable(context, new[] { Strings.Table_Name, Strings.Table_Description, Strings.Table_Priority }, host.GetFormatters().Select(f => new[] { f.Name, f.Description ?? "", f.Priority.ToString() })); break; case "renderers": - RenderTable(context, new[] { "Name", "Description" }, + RenderTable(context, new[] { Strings.Table_Name, Strings.Table_Description }, host.GetRenderers().Select(r => new[] { r.Name, r.Description ?? "" })); break; case "serializers": - RenderTable(context, new[] { "Format", "Extensions", "Name" }, + RenderTable(context, new[] { Strings.Table_Format, Strings.Table_Extensions, Strings.Table_Name }, host.GetSerializers().Select(s => new[] { s.FormatId, string.Join(", ", s.FileExtensions), s.Name })); break; case "extensions": - RenderTable(context, new[] { "Id", "Name", "Version", "Status" }, + RenderTable(context, new[] { Strings.Table_Id, Strings.Table_Name, Strings.Table_Version, Strings.Table_Status }, host.GetExtensionInfos().Select(e => new[] { e.ExtensionId, e.Name, e.Version, e.Status.ToString() })); break; case "exporters": - RenderTable(context, new[] { "Format", "Description" }, + RenderTable(context, new[] { Strings.Table_Format, Strings.Table_Description }, host.GetToolbarActions() .Where(a => a.Placement == ToolbarPlacement.ExportMenu) .OrderBy(a => a.Order) @@ -65,7 +69,9 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell break; default: - context.Console.MarkupLine($"[red]Unknown list kind '{Markup.Escape(kind)}'.[/] Valid: kernels, themes, formatters, renderers, serializers, extensions, exporters."); + context.Console.MarkupLine( + Messages.In("red", Messages.Say(Strings.Meta_List_UnknownKind, kind)) + + " " + Messages.Typed(Strings.Meta_List_ValidKinds, Kinds)); break; } @@ -77,7 +83,7 @@ private static void RenderTable(MetaContext context, string[] columns, IEnumerab var rowList = rows.ToList(); if (rowList.Count == 0) { - context.Console.MarkupLine("[dim]No items registered.[/]"); + context.Console.MarkupLine(Messages.In("dim", Messages.Say(Strings.Meta_List_Empty))); return; } @@ -93,13 +99,15 @@ private static void RenderTable(MetaContext context, string[] columns, IEnumerab { var widths = new int[columns.Length]; for (int i = 0; i < columns.Length; i++) - widths[i] = Math.Max(columns[i].Length, rowList.Max(r => i < r.Length ? r[i].Length : 0)); + widths[i] = Math.Max( + DisplayWidth.Measure(columns[i]), + rowList.Max(r => i < r.Length ? DisplayWidth.Measure(r[i]) : 0)); // In no-color mode Spectre's segment wrapping concatenates rows; use plain // Console.Out so each row lands on its own line. - Console.Out.WriteLine(string.Join(" ", columns.Select((c, i) => c.PadRight(widths[i])))); + Console.Out.WriteLine(string.Join(" ", columns.Select((c, i) => DisplayWidth.PadRight(c, widths[i])))); foreach (var row in rowList) - Console.Out.WriteLine(string.Join(" ", row.Select((c, i) => c.PadRight(widths[i])))); + Console.Out.WriteLine(string.Join(" ", row.Select((c, i) => DisplayWidth.PadRight(c, widths[i])))); } } } diff --git a/src/Verso.Cli/Repl/Meta/Commands/LoadMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/LoadMeta.cs index 03d7bf2a..fbc9bb1c 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/LoadMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/LoadMeta.cs @@ -1,5 +1,6 @@ using Spectre.Console; using Verso.Abstractions; +using Verso.Cli.Resources; using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -11,32 +12,33 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class LoadMeta : IMetaCommand { public string Name => "load"; - public string Summary => "Loads a notebook from disk, replacing the session notebook."; - public string DetailedHelp => - ".load \n" + - " Deserializes the file at through the matching serializer and installs it\n" + - " as the session notebook. Prompts to save unsaved changes first. Kernel state\n" + - " (variables) is preserved — run .reset first for a clean start."; + public string Summary => Strings.Meta_Load_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".load \n" + Strings.Meta_Load_Details; public async Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { var arg = argumentText.Trim(); if (string.IsNullOrEmpty(arg)) { - context.Console.MarkupLine("[red]Usage: .load [/]"); + context.Console.MarkupLine(Messages.In("red", Messages.Typed(Strings.Repl_Usage, ".load "))); return true; } if (!context.Session.ConfirmDiscardUnsavedChanges()) { - context.Console.MarkupLine("[yellow]Session has unsaved cells.[/] Run [bold].save[/] first, or [bold].load[/] again to discard."); + context.Console.MarkupLine( + Messages.In("yellow", Messages.Say(Strings.Repl_UnsavedCells)) + + " " + Messages.Typed(Strings.Repl_UnsavedHint, ".save", ".load")); return true; } var fullPath = Path.GetFullPath(arg); if (!File.Exists(fullPath)) { - context.Console.MarkupLine($"[red]File not found: {Markup.Escape(fullPath)}[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Say(Strings.Meta_Load_FileNotFound, fullPath))); return true; } @@ -47,7 +49,7 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C } catch (SerializerNotFoundException ex) { - context.Console.MarkupLine($"[red]{Markup.Escape(ex.Message)}[/]"); + context.Console.MarkupLine(Messages.In("red", Markup.Escape(ex.Message))); return true; } @@ -59,7 +61,8 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C } catch (Exception ex) { - context.Console.MarkupLine($"[red]Failed to load: {Markup.Escape(ex.Message)}[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Say(Strings.Meta_Load_Failed, ex.Message))); return true; } @@ -81,7 +84,8 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C context.Session.ActiveKernelId = current.DefaultKernelId ?? context.Session.ActiveKernelId; context.Session.MarkClean(); - context.Console.MarkupLine($"[green]Loaded[/] {current.Cells.Count} cell(s) from {Markup.Escape(fullPath)}"); + context.Console.MarkupLine(Messages.In("green", Messages.Say( + Strings.Meta_Load_Done, CellCount.Describe(current.Cells.Count), fullPath))); return true; } } diff --git a/src/Verso.Cli/Repl/Meta/Commands/MdMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/MdMeta.cs index a9f65fa0..a7f39504 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/MdMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/MdMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -6,16 +8,15 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class MdMeta : IMetaCommand { public string Name => "md"; - public string Summary => "Marks the next submission as a markdown cell."; - public string DetailedHelp => - ".md\n" + - " One-shot: the next submission is appended as a markdown cell instead of a code cell.\n" + - " After the cell is appended, the REPL reverts to code mode."; + public string Summary => Strings.Meta_Md_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".md\n" + Strings.Meta_Md_Details; public Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { context.Session.NextCellTypeOverride = "markdown"; - context.Console.MarkupLine("[dim]Next cell will be markdown.[/]"); + context.Console.MarkupLine(Messages.In("dim", Messages.Say(Strings.Meta_Md_Next))); return Task.FromResult(true); } } diff --git a/src/Verso.Cli/Repl/Meta/Commands/RecallMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/RecallMeta.cs index 9f912669..dbbf3394 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/RecallMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/RecallMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -6,32 +8,34 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class RecallMeta : IMetaCommand { public string Name => "recall"; - public string Summary => "Loads a prior cell's source into the prompt for editing."; - public string DetailedHelp => - ".recall \n" + - " Loads cell n's source into the prompt buffer as if the user had typed it.\n" + - " Pressing Enter submits it as a new cell; the original cell remains untouched.\n" + - " Out-of-range indices produce an error without clearing the buffer."; + public string Summary => Strings.Meta_Recall_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".recall \n" + Strings.Meta_Recall_Details; public Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { var arg = argumentText.Trim(); if (string.IsNullOrEmpty(arg) || !int.TryParse(arg, out var index) || index <= 0) { - context.Console.MarkupLine($"[red]Usage: .recall [/] where is a 1-based cell index (from .history)."); + context.Console.MarkupLine(Messages.In("red", + Messages.Typed(Strings.Meta_Recall_Usage, ".recall ", ".history"))); return Task.FromResult(true); } var cells = context.Session.Notebook.Cells; if (index > cells.Count) { - context.Console.MarkupLine($"[red]Cell [[{index}]] is out of range.[/] History contains {cells.Count} cell(s)."); + context.Console.MarkupLine( + Messages.In("red", Messages.Say(Strings.Meta_Recall_OutOfRange, index)) + + " " + Messages.Say(Strings.Meta_Recall_HistoryContains, CellCount.Describe(cells.Count))); return Task.FromResult(true); } var cell = cells[index - 1]; context.Session.PendingInitialText = cell.Source; - context.Console.MarkupLine($"[dim]Recalled cell [[{index}]]; edit then press Enter + blank line (or ;;) to submit.[/]"); + context.Console.MarkupLine(Messages.In("dim", + Messages.Say(Strings.Meta_Recall_Recalled, index))); return Task.FromResult(true); } } diff --git a/src/Verso.Cli/Repl/Meta/Commands/RerunMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/RerunMeta.cs index e19727c3..5a5eceac 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/RerunMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/RerunMeta.cs @@ -1,5 +1,7 @@ using Spectre.Console; using Verso.Abstractions; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; using Verso.Execution; namespace Verso.Cli.Repl.Meta.Commands; @@ -8,13 +10,13 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class RerunMeta : IMetaCommand { public string Name => "rerun"; - public string Summary => "Re-executes a prior cell (or a range) as new cells."; - public string DetailedHelp => - ".rerun [..]|all [--fail-fast]\n" + - " Re-executes cell n (or range n..m, or every cell with 'all') verbatim,\n" + - " appending each as a new cell. Does not mutate prior cells. A range submits\n" + - " cells individually so each renders its own outputs; failures within a range\n" + - " do not stop the rest unless --fail-fast."; + public string Summary => Strings.Meta_Rerun_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => Usage + "\n" + Strings.Meta_Rerun_Details; + + /// The shape a .rerun takes. Typed at a keyboard, so the same in every language. + private const string Usage = ".rerun [..]|all [--fail-fast]"; public async Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { @@ -24,7 +26,7 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C if (string.IsNullOrEmpty(range)) { - context.Console.MarkupLine("[red]Usage: .rerun [[..]]|all [[--fail-fast]][/]"); + context.Console.MarkupLine(Messages.In("red", Messages.Typed(Strings.Repl_Usage, Usage))); return true; } @@ -34,7 +36,7 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C { if (cells.Count == 0) { - context.Console.MarkupLine("[dim]No cells to rerun.[/]"); + context.Console.MarkupLine(Messages.In("dim", Messages.Say(Strings.Meta_Rerun_Nothing))); return true; } start = 1; @@ -45,7 +47,8 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C var pieces = range.Split("..", 2); if (!int.TryParse(pieces[0], out start) || !int.TryParse(pieces[1], out end) || start <= 0 || end < start) { - context.Console.MarkupLine($"[red]Invalid range '{Markup.Escape(range)}'.[/] Expected .. with m >= n."); + context.Console.MarkupLine(Messages.In("red", + Messages.Say(Strings.Meta_Rerun_InvalidRange, range))); return true; } } @@ -53,7 +56,8 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C { if (!int.TryParse(range, out start) || start <= 0) { - context.Console.MarkupLine($"[red]Invalid cell index '{Markup.Escape(range)}'.[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Say(Strings.Meta_Rerun_InvalidIndex, range))); return true; } end = start; @@ -61,7 +65,8 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C if (end > cells.Count) { - context.Console.MarkupLine($"[red]Range [[{start}..{end}]] exceeds history length ({cells.Count}).[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Say(Strings.Meta_Rerun_RangeTooLong, start, end, cells.Count))); return true; } @@ -95,12 +100,13 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C } catch (OperationCanceledException) { - context.Console.MarkupLine("[yellow]Cancelled.[/]"); + context.Console.MarkupLine(Messages.In("yellow", Messages.Say(Strings.Repl_Cancelled))); break; } catch (Exception ex) { - context.Console.MarkupLine($"[red]Execution error in rerun [[{i}]]:[/] {Markup.Escape(ex.Message)}"); + context.Console.MarkupLine(Messages.In("red", + Messages.Say(Strings.Meta_Rerun_ExecutionError, i, ex.Message))); if (failFast) break; continue; } diff --git a/src/Verso.Cli/Repl/Meta/Commands/ResetMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/ResetMeta.cs index e8ff991c..525bc76f 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/ResetMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/ResetMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -6,17 +8,15 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class ResetMeta : IMetaCommand { public string Name => "reset"; - public string Summary => "Resets kernel state; keeps cell history."; - public string DetailedHelp => - ".reset\n" + - " Rebuilds the kernel session, clearing all variables and runtime state.\n" + - " The notebook's cell history (cells already typed) is preserved, so .save\n" + - " still captures them. Variables declared before .reset are gone."; + public string Summary => Strings.Meta_Reset_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".reset\n" + Strings.Meta_Reset_Details; public async Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { await context.Session.ResetScaffoldAsync(); - context.Console.MarkupLine("[green]Kernel state reset.[/]"); + context.Console.MarkupLine(Messages.In("green", Messages.Say(Strings.Meta_Reset_Done))); return true; } } diff --git a/src/Verso.Cli/Repl/Meta/Commands/SaveMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/SaveMeta.cs index 2ce69296..8a3a9651 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/SaveMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/SaveMeta.cs @@ -1,5 +1,6 @@ using Spectre.Console; using Verso.Abstractions; +using Verso.Cli.Resources; using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -8,13 +9,10 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class SaveMeta : IMetaCommand { public string Name => "save"; - public string Summary => "Writes the session notebook to disk."; - public string DetailedHelp => - ".save []\n" + - " Serializes the session notebook. When is omitted, saves to the original\n" + - " loaded path (if any) or reports an error. Format is inferred from the extension.\n" + - " Without --preserve-format, a .save with no arg against an .ipynb-loaded notebook\n" + - " converts to a sibling .verso file; with --preserve-format the original format is kept."; + public string Summary => Strings.Meta_Save_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".save []\n" + Strings.Meta_Save_Details; public async Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { @@ -24,7 +22,9 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C if (string.IsNullOrEmpty(targetPath)) { - context.Console.MarkupLine("[red].save requires a path when the session has no loaded notebook.[/] Usage: .save "); + context.Console.MarkupLine( + Messages.In("red", Messages.Say(Strings.Meta_Save_NeedsPath)) + + " " + Messages.Typed(Strings.Repl_Usage, ".save ")); return true; } @@ -40,8 +40,8 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C && !preservesByDefault && !targetPath.EndsWith(".verso", StringComparison.OrdinalIgnoreCase)) { - context.Console.MarkupLine( - $"[yellow]Converting to .verso; use --preserve-format to keep[/] {Markup.Escape(Path.GetExtension(targetPath))}."); + context.Console.MarkupLine(Messages.In("yellow", + Messages.Say(Strings.Meta_Save_Converting, Path.GetExtension(targetPath)))); targetPath = Path.ChangeExtension(targetPath, ".verso"); } @@ -52,7 +52,7 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C } catch (SerializerNotFoundException ex) { - context.Console.MarkupLine($"[red]{Markup.Escape(ex.Message)}[/]"); + context.Console.MarkupLine(Messages.In("red", Markup.Escape(ex.Message))); return true; } @@ -66,11 +66,13 @@ public async Task ExecuteAsync(string argumentText, MetaContext context, C await File.WriteAllTextAsync(targetPath, content, ct); context.Session.NotebookPath = targetPath; context.Session.MarkClean(); - context.Console.MarkupLine($"[green]Saved[/] {context.Session.Notebook.Cells.Count} cell(s) to {Markup.Escape(targetPath)}"); + context.Console.MarkupLine(Messages.In("green", Messages.Say( + Strings.Meta_Save_Done, CellCount.Describe(context.Session.Notebook.Cells.Count), targetPath))); } catch (Exception ex) { - context.Console.MarkupLine($"[red]Failed to save: {Markup.Escape(ex.Message)}[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Say(Strings.Meta_Save_Failed, ex.Message))); } return true; diff --git a/src/Verso.Cli/Repl/Meta/Commands/SetMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/SetMeta.cs index 1a90afa4..a4cdad2b 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/SetMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/SetMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -6,18 +8,25 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class SetMeta : IMetaCommand { public string Name => "set"; - public string Summary => "Sets a runtime REPL setting (preview.rows, preview.lines, preview.elapsedThresholdMs)."; - public string DetailedHelp => - ".set \n" + - " Updates one of the runtime REPL settings.\n" + - " Known keys: preview.rows, preview.lines, preview.elapsedThresholdMs, confirmOnExit."; + public string Summary => Strings.Meta_Set_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => Usage + "\n" + Strings.Meta_Set_Details; + + /// The shape a .set takes. Typed at a keyboard, so the same in every language. + private const string Usage = ".set "; + + /// The settings .set understands. Identifiers, so the same in every language. + private const string Keys = "preview.rows, preview.lines, preview.elapsedThresholdMs, confirmOnExit"; public Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { var parts = argumentText.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length != 2) { - context.Console.MarkupLine("[red]Usage: .set [/] — try [bold].help set[/] for key list."); + context.Console.MarkupLine( + Messages.In("red", Messages.Typed(Strings.Repl_Usage, Usage)) + + " " + Messages.Typed(Strings.Meta_Set_HelpHint, ".help set")); return Task.FromResult(true); } @@ -34,7 +43,8 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell context.Console.MarkupLine($"preview.rows = [bold]{rows}[/]"); } else - context.Console.MarkupLine($"[red]Invalid integer for preview.rows: '{Markup.Escape(value)}'[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Typed(Strings.Meta_Set_InvalidInteger, "preview.rows", value))); break; case "preview.lines": @@ -44,7 +54,8 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell context.Console.MarkupLine($"preview.lines = [bold]{lines}[/]"); } else - context.Console.MarkupLine($"[red]Invalid integer for preview.lines: '{Markup.Escape(value)}'[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Typed(Strings.Meta_Set_InvalidInteger, "preview.lines", value))); break; case "preview.elapsedthresholdms": @@ -54,7 +65,8 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell context.Console.MarkupLine($"preview.elapsedThresholdMs = [bold]{ms}[/]"); } else - context.Console.MarkupLine($"[red]Invalid non-negative integer for preview.elapsedThresholdMs: '{Markup.Escape(value)}'[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Typed(Strings.Meta_Set_InvalidNonNegative, "preview.elapsedThresholdMs", value))); break; case "confirmonexit": @@ -64,11 +76,14 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell context.Console.MarkupLine($"confirmOnExit = [bold]{confirm}[/]"); } else - context.Console.MarkupLine($"[red]Invalid boolean for confirmOnExit: '{Markup.Escape(value)}'[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Typed(Strings.Meta_Set_InvalidBoolean, "confirmOnExit", value))); break; default: - context.Console.MarkupLine($"[red]Unknown setting key '{Markup.Escape(key)}'.[/] Known: preview.rows, preview.lines, preview.elapsedThresholdMs, confirmOnExit."); + context.Console.MarkupLine( + Messages.In("red", Messages.Say(Strings.Meta_Set_UnknownKey, key)) + + " " + Messages.Typed(Strings.Meta_Set_KnownKeys, Keys)); break; } diff --git a/src/Verso.Cli/Repl/Meta/Commands/ThemeMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/ThemeMeta.cs index e0fe5a34..348c5c05 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/ThemeMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/ThemeMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -6,20 +8,18 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class ThemeMeta : IMetaCommand { public string Name => "theme"; - public string Summary => "Prints or switches the active theme."; - public string DetailedHelp => - ".theme []\n" + - " With no argument, prints the active theme.\n" + - " With a name, changes the active theme. Matched by DisplayName case-insensitively,\n" + - " with ThemeId as a fallback."; + public string Summary => Strings.Meta_Theme_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".theme []\n" + Strings.Meta_Theme_Details; public Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { var arg = argumentText.Trim(); if (string.IsNullOrEmpty(arg)) { - var current = context.Session.ActiveTheme?.DisplayName ?? ""; - context.Console.MarkupLine($"Active theme: [bold]{Markup.Escape(current)}[/]"); + var current = context.Session.ActiveTheme?.DisplayName ?? Strings.Repl_MarkerDefault; + context.Console.MarkupLine(Messages.Typed(Strings.Meta_Theme_Active, current)); return Task.FromResult(true); } @@ -30,12 +30,14 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell if (match is null) { var known = string.Join(", ", themes.Select(t => t.DisplayName)); - context.Console.MarkupLine($"[red]Theme '{Markup.Escape(arg)}' is not registered.[/] Available: {Markup.Escape(known)}"); + context.Console.MarkupLine( + Messages.In("red", Messages.Say(Strings.Error_ThemeNotRegistered, arg)) + + " " + Messages.Say(Strings.Error_AvailableThemes, known)); return Task.FromResult(true); } context.Session.ActiveTheme = match; - context.Console.MarkupLine($"Switched to theme: [bold]{Markup.Escape(match.DisplayName)}[/]"); + context.Console.MarkupLine(Messages.Typed(Strings.Meta_Theme_Switched, match.DisplayName)); return Task.FromResult(true); } } diff --git a/src/Verso.Cli/Repl/Meta/Commands/VarsMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/VarsMeta.cs index 2ed449cb..ce9adbfc 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/VarsMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/VarsMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -6,10 +8,10 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class VarsMeta : IMetaCommand { public string Name => "vars"; - public string Summary => "Lists variables from IVariableStore."; - public string DetailedHelp => - ".vars\n" + - " Lists variables from the shared IVariableStore. Columns: Name, Type, Preview."; + public string Summary => Strings.Meta_Vars_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".vars\n" + Strings.Meta_Vars_Details; public Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { @@ -17,16 +19,16 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell if (variables.Count == 0) { - context.Console.MarkupLine("[dim]No variables.[/]"); + context.Console.MarkupLine(Messages.In("dim", Messages.Say(Strings.Meta_Vars_Empty))); return Task.FromResult(true); } if (context.UseColor) { var table = new Table().Border(TableBorder.Rounded); - table.AddColumn("Name"); - table.AddColumn("Type"); - table.AddColumn("Preview"); + table.AddColumn(Strings.Table_Name); + table.AddColumn(Strings.Table_Type); + table.AddColumn(Strings.Table_Preview); foreach (var v in variables) { table.AddRow( @@ -38,7 +40,8 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell } else { - Console.Out.WriteLine($"{"NAME",-20} {"TYPE",-24} PREVIEW"); + Console.Out.WriteLine( + $"{Truncate(Strings.Table_Name, 20),-20} {Truncate(Strings.Table_Type, 24),-24} {Strings.Table_Preview}"); foreach (var v in variables) { Console.Out.WriteLine( diff --git a/src/Verso.Cli/Repl/Meta/Commands/ViewMeta.cs b/src/Verso.Cli/Repl/Meta/Commands/ViewMeta.cs index 6ae2ede5..0ed83484 100644 --- a/src/Verso.Cli/Repl/Meta/Commands/ViewMeta.cs +++ b/src/Verso.Cli/Repl/Meta/Commands/ViewMeta.cs @@ -1,4 +1,6 @@ using Spectre.Console; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Meta.Commands; @@ -6,18 +8,17 @@ namespace Verso.Cli.Repl.Meta.Commands; public sealed class ViewMeta : IMetaCommand { public string Name => "view"; - public string Summary => "Prints the last cell's output without truncation."; - public string DetailedHelp => - ".view []\n" + - " Prints the outputs of the last cell (or cell n if supplied) in full, bypassing\n" + - " the row/line caps applied during normal rendering."; + public string Summary => Strings.Meta_View_Summary; + + // The first line is what the reader types, so it is written here rather than translated. + public string DetailedHelp => ".view []\n" + Strings.Meta_View_Details; public Task ExecuteAsync(string argumentText, MetaContext context, CancellationToken ct) { var cells = context.Session.Notebook.Cells; if (cells.Count == 0) { - context.Console.MarkupLine("[dim]No cells to view.[/]"); + context.Console.MarkupLine(Messages.In("dim", Messages.Say(Strings.Meta_View_Nothing))); return Task.FromResult(true); } @@ -27,7 +28,8 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell { if (!int.TryParse(arg, out var parsed) || parsed <= 0 || parsed > cells.Count) { - context.Console.MarkupLine($"[red]Invalid or out-of-range cell index '{Markup.Escape(arg)}'.[/]"); + context.Console.MarkupLine(Messages.In("red", + Messages.Say(Strings.Meta_View_InvalidIndex, arg))); return Task.FromResult(true); } index = parsed - 1; @@ -36,7 +38,8 @@ public Task ExecuteAsync(string argumentText, MetaContext context, Cancell var cell = cells[index]; if (cell.Outputs.Count == 0) { - context.Console.MarkupLine($"[dim]Cell [[{index + 1}]] has no outputs.[/]"); + context.Console.MarkupLine(Messages.In("dim", + Messages.Say(Strings.Meta_View_NoOutputs, index + 1))); return Task.FromResult(true); } diff --git a/src/Verso.Cli/Repl/Prompt/PrettyPromptDriver.cs b/src/Verso.Cli/Repl/Prompt/PrettyPromptDriver.cs index a796e7aa..2f468ade 100644 --- a/src/Verso.Cli/Repl/Prompt/PrettyPromptDriver.cs +++ b/src/Verso.Cli/Repl/Prompt/PrettyPromptDriver.cs @@ -2,6 +2,8 @@ using PpPrompt = PrettyPrompt.Prompt; using PpConfiguration = PrettyPrompt.PromptConfiguration; +using Verso.Cli.Resources; + namespace Verso.Cli.Repl.Prompt; /// @@ -39,7 +41,7 @@ public async Task ReadAsync(int inputCounter, string? activeKernelId, if (!string.IsNullOrEmpty(initialText)) { Console.WriteLine(); - Console.WriteLine("(recalled — copy and paste or edit below)"); + Console.WriteLine(Strings.Prompt_Recalled); foreach (var line in initialText.Split('\n')) Console.WriteLine(" " + line.TrimEnd('\r')); Console.WriteLine(); diff --git a/src/Verso.Cli/Repl/Rendering/MimeDispatcher.cs b/src/Verso.Cli/Repl/Rendering/MimeDispatcher.cs index 4163b167..9d3e77e8 100644 --- a/src/Verso.Cli/Repl/Rendering/MimeDispatcher.cs +++ b/src/Verso.Cli/Repl/Rendering/MimeDispatcher.cs @@ -2,6 +2,8 @@ using Spectre.Console.Rendering; using Verso.Abstractions; using Verso.Cli.Repl.Rendering.Renderers; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Rendering; @@ -37,7 +39,7 @@ public IRenderable AsRenderable(CellOutput output, int cellCounter, int outputIn // A widget only exists once a browser has run it, so a terminal says it is there // rather than printing the page it would have run. CellOutput.WidgetMimeType => PlainTextRenderer.AsRenderable( - output with { Content = "[widget output, shown when the notebook is opened]" }, policy), + output with { Content = Strings.Render_WidgetPlaceholder }, policy), // A terminal has no live bar to show once the cell is done, so the last reported // state is rendered as one line. CellOutput.ProgressMimeType => PlainTextRenderer.AsRenderable( @@ -59,12 +61,14 @@ private IRenderable BuildUnknown(CellOutput output, TruncationPolicy policy) var isProbablyText = text.Length == 0 || text.Take(256).All(c => c >= ' ' || c == '\n' || c == '\r' || c == '\t'); if (!isProbablyText) { - var msg = $""; - return _useColor ? new Markup($"[dim]{Markup.Escape(msg)}[/]") : new Text(msg); + var msg = string.Format(Strings.Render_BinaryOutput, output.MimeType, text.Length); + return _useColor ? new Markup(Messages.In("dim", Markup.Escape(msg))) : new Text(msg); } - var header = $""; - var headerRenderable = _useColor ? (IRenderable)new Markup($"[dim]{Markup.Escape(header)}[/]") : new Text(header); + var header = string.Format(Strings.Render_UnknownOutput, output.MimeType); + var headerRenderable = _useColor + ? (IRenderable)new Markup(Messages.In("dim", Markup.Escape(header))) + : new Text(header); return new Rows(headerRenderable, new Text(policy.ClipLines(text))); } } diff --git a/src/Verso.Cli/Repl/Rendering/Renderers/CsvTableRenderer.cs b/src/Verso.Cli/Repl/Rendering/Renderers/CsvTableRenderer.cs index 312ba55c..d7a5147f 100644 --- a/src/Verso.Cli/Repl/Rendering/Renderers/CsvTableRenderer.cs +++ b/src/Verso.Cli/Repl/Rendering/Renderers/CsvTableRenderer.cs @@ -1,6 +1,8 @@ using Spectre.Console; using Spectre.Console.Rendering; using Verso.Abstractions; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Rendering.Renderers; @@ -41,7 +43,11 @@ public static IRenderable AsRenderable(CellOutput output, TruncationPolicy polic } if (body.Count > rowCap) - return new Rows(table, new Markup($"[dim]… {body.Count - rowCap} more rows[/]")); + { + var omitted = body.Count - rowCap; + return new Rows(table, new Markup(Messages.In("dim", Messages.Say( + Plural.Of(omitted, Strings.Render_MoreRows_One, Strings.Render_MoreRows_Other), omitted)))); + } return table; } diff --git a/src/Verso.Cli/Repl/Rendering/Renderers/ImagePlaceholderRenderer.cs b/src/Verso.Cli/Repl/Rendering/Renderers/ImagePlaceholderRenderer.cs index d3e2d5b1..f8d2de18 100644 --- a/src/Verso.Cli/Repl/Rendering/Renderers/ImagePlaceholderRenderer.cs +++ b/src/Verso.Cli/Repl/Rendering/Renderers/ImagePlaceholderRenderer.cs @@ -1,6 +1,8 @@ using Spectre.Console; using Spectre.Console.Rendering; using Verso.Abstractions; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; namespace Verso.Cli.Repl.Rendering.Renderers; @@ -36,19 +38,22 @@ public static IRenderable AsRenderable(CellOutput output, int cellCounter, int i } catch (Exception ex) { - var msg = $""; + var msg = string.Format(Strings.Render_ImageFailed, output.MimeType, ex.Message); return useColor - ? new Markup($"[yellow]{Markup.Escape(msg)}[/]") + ? new Markup(Messages.In("yellow", Markup.Escape(msg))) : new Text(msg); } var info = new FileInfo(path); var size = FormatSize(info.Length); - if (useColor) - return new Markup( - $"[dim][/]"); - return new Text($""); + var described = string.Format(Strings.Render_Image, output.MimeType, size, path); + + // The whole line is dimmed rather than the path picked out in another colour. Where the + // path falls in the sentence is the translator's to decide, so nothing here can know + // which part to colour. + return useColor + ? new Markup(Messages.In("dim", Markup.Escape(described))) + : new Text(described); } private static string FormatSize(long bytes) diff --git a/src/Verso.Cli/Repl/Rendering/TerminalRenderer.cs b/src/Verso.Cli/Repl/Rendering/TerminalRenderer.cs index 3ed98c2d..b9cdb4d4 100644 --- a/src/Verso.Cli/Repl/Rendering/TerminalRenderer.cs +++ b/src/Verso.Cli/Repl/Rendering/TerminalRenderer.cs @@ -2,6 +2,8 @@ using Spectre.Console.Rendering; using Verso.Abstractions; using Verso.Cli.Repl.Settings; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; using Verso.Execution; using Verso.Extensions.Utilities; @@ -114,7 +116,7 @@ public void RenderCell(int inputCounter, CellModel cell, ExecutionResult result, private IRenderable BuildResultErrorRenderable(ExecutionResult result) { - var message = result.Error?.Message ?? "Execution failed."; + var message = result.Error?.Message ?? Strings.Render_ExecutionFailed; var name = result.Error?.GetType().Name; var stack = result.Error?.StackTrace; @@ -138,10 +140,11 @@ private IRenderable BuildResultErrorRenderable(ExecutionResult result) private void MaybePrintElapsed(ExecutionResult result, TimeSpan threshold) { if (result.Elapsed < threshold || result.Elapsed <= TimeSpan.Zero) return; + var elapsed = string.Format(Strings.Render_ExecutedIn, FormatElapsed(result.Elapsed)); if (_useColor) - _console.MarkupLine($"[dim](executed in {FormatElapsed(result.Elapsed)})[/]"); + _console.MarkupLine(Messages.In("dim", Markup.Escape(elapsed))); else - _console.WriteLine($"(executed in {FormatElapsed(result.Elapsed)})"); + _console.WriteLine(elapsed); } private static string FormatElapsed(TimeSpan elapsed) diff --git a/src/Verso.Cli/Repl/Rendering/TruncationPolicy.cs b/src/Verso.Cli/Repl/Rendering/TruncationPolicy.cs index 4394dcb4..9dac3b49 100644 --- a/src/Verso.Cli/Repl/Rendering/TruncationPolicy.cs +++ b/src/Verso.Cli/Repl/Rendering/TruncationPolicy.cs @@ -1,4 +1,6 @@ +using Verso.Abstractions; using Verso.Cli.Repl.Settings; +using Verso.Cli.Resources; namespace Verso.Cli.Repl.Rendering; @@ -24,6 +26,8 @@ public string ClipLines(string text) var lines = text.Split('\n'); if (lines.Length <= MaxLines) return text; var kept = lines.Take(MaxLines); - return string.Join('\n', kept) + $"\n… {lines.Length - MaxLines} more lines"; + var omitted = lines.Length - MaxLines; + return string.Join('\n', kept) + "\n" + string.Format( + Plural.Of(omitted, Strings.Render_ClippedLines_One, Strings.Render_ClippedLines_Other), omitted); } } diff --git a/src/Verso.Cli/Repl/ReplLoop.cs b/src/Verso.Cli/Repl/ReplLoop.cs index 9b6789b1..485d5ee5 100644 --- a/src/Verso.Cli/Repl/ReplLoop.cs +++ b/src/Verso.Cli/Repl/ReplLoop.cs @@ -4,6 +4,8 @@ using Verso.Cli.Repl.Meta.Commands; using Verso.Cli.Repl.Prompt; using Verso.Cli.Repl.Rendering; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; using Verso.Execution; namespace Verso.Cli.Repl; @@ -39,31 +41,47 @@ public ReplLoop( _useColor = useColor; _elapsedThreshold = elapsedThreshold; - _metaRegistry = new MetaCommandRegistry(); - _metaRegistry.Register(new HelpMeta()); - _metaRegistry.Register(new ExitMeta()); - _metaRegistry.Register(new ClearMeta()); - _metaRegistry.Register(new ResetMeta()); - _metaRegistry.Register(new KernelMeta()); - _metaRegistry.Register(new VarsMeta()); - _metaRegistry.Register(new ListMeta()); - _metaRegistry.Register(new ThemeMeta()); - _metaRegistry.Register(new LayoutMeta()); - _metaRegistry.Register(new MdMeta()); - _metaRegistry.Register(new HistoryMeta()); - _metaRegistry.Register(new RecallMeta()); - _metaRegistry.Register(new RerunMeta()); - _metaRegistry.Register(new SetMeta()); - _metaRegistry.Register(new ViewMeta()); - _metaRegistry.Register(new SaveMeta()); - _metaRegistry.Register(new LoadMeta()); - _metaRegistry.Register(new ConvertMeta()); - _metaRegistry.Register(new ExportMeta()); + _metaRegistry = CreateDefaultRegistry(); + } + + /// + /// The meta-commands a session starts with, in the order .help lists them. + /// + /// + /// Separate from the constructor so the set can be read without a running session behind it, + /// which is how the tests reach every command's help text. + /// + public static MetaCommandRegistry CreateDefaultRegistry() + { + var registry = new MetaCommandRegistry(); + registry.Register(new HelpMeta()); + registry.Register(new ExitMeta()); + registry.Register(new ClearMeta()); + registry.Register(new ResetMeta()); + registry.Register(new KernelMeta()); + registry.Register(new VarsMeta()); + registry.Register(new ListMeta()); + registry.Register(new ThemeMeta()); + registry.Register(new LayoutMeta()); + registry.Register(new MdMeta()); + registry.Register(new HistoryMeta()); + registry.Register(new RecallMeta()); + registry.Register(new RerunMeta()); + registry.Register(new SetMeta()); + registry.Register(new ViewMeta()); + registry.Register(new SaveMeta()); + registry.Register(new LoadMeta()); + registry.Register(new ConvertMeta()); + registry.Register(new ExportMeta()); + return registry; } /// Exposed for tests and future phases to register additional meta-commands. public MetaCommandRegistry MetaRegistry => _metaRegistry; + /// What the reader types to see the REPL's commands. The same in every language. + private const string HelpCommand = ".help"; + public async Task RunAsync(CancellationToken ct) { var metaContext = new MetaContext(_session, _console, _renderer, _metaRegistry, _useColor); @@ -115,7 +133,9 @@ public async Task RunAsync(CancellationToken ct) if (!_metaRegistry.TryResolve(name, out var metaCommand)) { - _console.MarkupLine($"[red]Unknown meta-command '.{Markup.Escape(name)}'.[/] Type [bold].help[/] for the list."); + _console.MarkupLine( + Messages.In("red", Messages.Say(Strings.Repl_UnknownMetaCommand, name)) + + " " + Messages.Typed(Strings.Repl_TypeHelpForList, HelpCommand)); await _prompt.AddHistoryAsync(text); continue; } @@ -128,7 +148,8 @@ public async Task RunAsync(CancellationToken ct) } catch (Exception ex) { - _console.MarkupLine($"[red]Error in meta-command '.{Markup.Escape(name)}':[/] {Markup.Escape(ex.Message)}"); + _console.MarkupLine(Messages.In("red", + Messages.Say(Strings.Repl_MetaCommandError, name, ex.Message))); continue; } if (!keepRunning) @@ -172,12 +193,12 @@ private async Task ExecuteCellAsync(string source, CancellationToken ct) } catch (OperationCanceledException) { - _console.MarkupLine("[yellow]Cancelled.[/]"); + _console.MarkupLine(Messages.In("yellow", Messages.Say(Strings.Repl_Cancelled))); return; } catch (Exception ex) { - _console.MarkupLine($"[red]Execution error:[/] {Markup.Escape(ex.Message)}"); + _console.MarkupLine(Messages.In("red", Messages.Say(Strings.Repl_ExecutionError, ex.Message))); return; } diff --git a/src/Verso.Cli/Resources/Strings.de.resx b/src/Verso.Cli/Resources/Strings.de.resx new file mode 100644 index 00000000..aa5e893f --- /dev/null +++ b/src/Verso.Cli/Resources/Strings.de.resx @@ -0,0 +1,853 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Pfad zur Quell-Notebook-Datei. + + + Zwischen Notebook-Formaten konvertieren. + + + '{0}' wurde in '{1}' konvertiert + + + Nicht unterstütztes Format '{0}'. Unterstützt: {1} + + + Die Konvertierung in das Format '{0}' wird noch nicht unterstützt. + + + Pfad der Ausgabedatei. Standardmäßig der Name der Eingabedatei mit der neuen Endung. + + + Alle Zellenausgaben aus dem konvertierten Notebook entfernen. + + + Zielformat: verso, ipynb, md oder dib. + + + Die Nachbearbeitung ist fehlgeschlagen: {0} + + + Die Serialisierung ist fehlgeschlagen: {0} + + + Verfügbare Kernel: {0}. + + + Verfügbare Designs: {0}. + + + '{0}' konnte nicht deserialisiert werden: {1} + + + Das Exportformat '{0}' ist nicht registriert. + + + Eingabedatei nicht gefunden: {0} + + + Der Kernel '{0}' ist nicht registriert. + + + Mehrere Exportaktionen haben den Anzeigenamen '{0}'. Eindeutig über ActionId: {1}. + + + Mehrere Designs haben den Anzeigenamen '{0}'. Eindeutig über ThemeId: {1}. + + + Notebook-Datei nicht gefunden: {0} + + + Das Design '{0}' ist nicht registriert. + + + Nicht unterstütztes Notebook-Format '{0}'. Unterstützte Formate: {1} + + + Nicht unterstütztes Ausgabeformat '{0}'. Unterstützte Formate: {1} + + + Die Exportaktion '{0}' hat eine Ausnahme ausgelöst: {1} + + + Ein Notebook über eine ExportMenu-Symbolleistenaktion exportieren. + + + '{0}' wurde nach '{1}' exportiert + + + Bei der Ausführung des Notebooks sind Fehler aufgetreten. Der Export wird abgebrochen. + + + --format ist erforderlich. + + + <input> ist erforderlich, sofern nicht --list angegeben wird. + + + Es sind keine Exportaktionen registriert. + + + Die Exportaktion '{0}' hat keine Datei erzeugt. + + + Das Notebook vor dem Export ausführen, damit gespeicherte Ausgaben aktualisiert werden. + + + Exportformat, abgeglichen mit dem DisplayName einer registrierten IToolbarAction mit der Platzierung ExportMenu. Groß- und Kleinschreibung wird nicht beachtet. Werte mit Leerzeichen in Anführungszeichen setzen. Mit --list werden die installierten Formate angezeigt. + + + Layout-ID, die beim Export angewendet wird und im Aktionskontext als ActiveLayoutId bereitsteht. + + + Registrierte Exportaktionen auflisten (DisplayName, ActionId, Description) und beenden. + + + Registrierte Designs auflisten (DisplayName, Kind, Description) und beenden. + + + Pfad der Ausgabedatei. Ohne Angabe wird der vom Exporter vorgeschlagene Dateiname in das aktuelle Verzeichnis geschrieben. + + + DisplayName eines registrierten Designs, ohne Beachtung der Groß- und Kleinschreibung abgeglichen. Werte mit Leerzeichen in Anführungszeichen setzen. ThemeId wird ersatzweise akzeptiert, um gleiche Anzeigenamen zu unterscheiden. Mit --list-themes werden die installierten Designs angezeigt. + + + Führen Sie '{0}' aus, um Einzelheiten zu erhalten. + + + Führen Sie '{0}' aus, um die verfügbaren Formate anzuzeigen. + + + Version, Laufzeitumgebung und Erweiterungen der Verso-CLI anzeigen. + + + Erweiterungen: + + + Formatierer: + + + Serialisierer: + + + Engine: + + + Laufzeit: + + + Es sind keine Kernel registriert. + + + Es sind keine Designs registriert. + + + Leert den Terminalbildschirm. Der Sitzungszustand (Kernelvariablen, Notebook- + Zellen) bleibt erhalten; nur der Verlauf wird geleert. + + + Leert das Terminal. + + + Serialisiert das aktuelle Sitzungs-Notebook nach <path> mit dem Serialisierer, + dessen FileExtensions die Zielendung enthalten. Der geladene Pfad der Sitzung + ändert sich nicht. Gleiche Auflösung wie bei 'verso convert'. + + + Das Sitzungs-Notebook wurde nach {0} konvertiert ({1}) + + + Konvertieren fehlgeschlagen: {0} + + + Schreibt das Sitzungs-Notebook nach <path> mit dem zur Endung passenden Serialisierer. + + + Beendet die REPL. Bei nicht gespeicherten Zellen wird nachgefragt, + sofern confirmOnExit in den Benutzereinstellungen nicht deaktiviert ist. + + + Beendet die REPL. + + + Ruft eine IToolbarAction auf, die mit ToolbarPlacement.ExportMenu registriert ist. + Das Format wird über DisplayName abgeglichen (ohne Groß-/Kleinschreibung), ersatzweise ActionId. + Das Design wird über DisplayName abgeglichen (ohne Groß-/Kleinschreibung), ersatzweise ThemeId. + Ohne --output wird der vorgeschlagene Dateiname in das aktuelle Verzeichnis geschrieben. + Identisch mit 'verso export'. + + + Das Sitzungs-Notebook wurde nach {0} exportiert ({1}) + + + Fehlender Wert für {0}. + + + Exportiert das Sitzungs-Notebook über eine ExportMenu-Symbolleistenaktion. + + + Unbekanntes Argument: {0} + + + Ohne Argument wird eine Übersicht aller Meta-Befehle ausgegeben. + Mit einem Namen wird ausführliche Hilfe zu diesem Befehl ausgegeben. + + + Gibt Hilfe zu den Meta-Befehlen aus. + + + Gibt die letzten n übermittelten Zellen aus (Standard: 20). Jeder Eintrag zeigt + den Eingabezähler und eine Vorschau der ersten nicht leeren Quelltextzeile. + + + Kein Verlauf. + + + Ungültige Anzahl '{0}'. + + + Gibt die zuletzt übermittelten Zellen aus. + + + Aktiver Kernel: {0} + + + Ohne Argument wird der aktive Kernel ausgegeben. + Mit einer ID (LanguageId, ohne Beachtung der Groß-/Kleinschreibung) wird der aktive + Kernel für die folgenden Zellen gewechselt. Bereits im vorherigen Kernel deklarierte + Variablen bleiben in dessen Gültigkeitsbereich. + + + Gibt den aktiven Kernel aus oder wechselt ihn. + + + Zum Kernel gewechselt: {0} ({1}) + + + Das Vorwärmen des Kernels ist fehlgeschlagen: {0} + + + Aktives Layout: {0} + + + Layout zurückgesetzt. + + + Ohne Argument wird die ID des aktiven Layouts ausgegeben. + Mit einer ID wird die Standard-ActiveLayoutId für nachfolgende .export-Aufrufe gesetzt. + Mit 'none' wird das Layout zurückgesetzt. + + + Layout gesetzt auf: {0} + + + Gibt das Standard-Exportlayout aus oder setzt es. + + + Wobei <kind> eines der folgenden ist: + kernels, themes, formatters, renderers, serializers, extensions, exporters + Gibt eine Tabelle der registrierten Einträge für diese Fähigkeit aus. + + + Keine Einträge registriert. + + + Listet die registrierten Fähigkeiten der Erweiterungen auf. + + + Unbekannte Listenart '{0}'. + + + Gültige Arten: {0}. + + + Deserialisiert die Datei unter <path> mit dem passenden Serialisierer und setzt sie + als Sitzungs-Notebook ein. Nicht gespeicherte Änderungen werden zuvor abgefragt. + Der Kernelzustand (Variablen) bleibt erhalten: Führen Sie für einen sauberen Start + zuerst .reset aus. + + + {0} aus {1} geladen + + + Laden fehlgeschlagen: {0} + + + Datei nicht gefunden: {0} + + + Lädt ein Notebook von der Festplatte und ersetzt damit das Sitzungs-Notebook. + + + Einmalig: Die nächste Übermittlung wird als Markdown-Zelle statt als Codezelle angefügt. + Danach kehrt die REPL in den Codemodus zurück. + + + Die nächste Zelle wird eine Markdown-Zelle. + + + Markiert die nächste Übermittlung als Markdown-Zelle. + + + Lädt den Quelltext von Zelle n in den Eingabepuffer, als wäre er getippt worden. + Mit Enter wird er als neue Zelle übermittelt; die ursprüngliche Zelle bleibt unverändert. + Indizes außerhalb des Bereichs erzeugen einen Fehler, ohne den Puffer zu leeren. + + + Der Verlauf enthält {0}. + + + Zelle [{0}] liegt außerhalb des Bereichs. + + + Zelle [{0}] zurückgeholt. Bearbeiten Sie sie und drücken Sie dann Enter und eine Leerzeile (oder ;;) zum Übermitteln. + + + Lädt den Quelltext einer früheren Zelle zum Bearbeiten in die Eingabezeile. + + + Verwendung: {0}, wobei <n> ein ab eins gezählter Zellenindex ist, wie von {1} angezeigt. + + + Führt Zelle n (oder den Bereich n..m, oder mit 'all' jede Zelle) unverändert erneut + aus und fügt jede als neue Zelle an. Frühere Zellen bleiben unverändert. Bei einem + Bereich wird jede Zelle einzeln übermittelt, damit sie ihre eigenen Ausgaben zeigt; + Fehler innerhalb eines Bereichs halten die übrigen nicht auf, außer bei --fail-fast. + + + Ausführungsfehler beim erneuten Ausführen von [{0}]: {1} + + + Ungültiger Zellenindex '{0}'. + + + Ungültiger Bereich '{0}'. Erwartet wird <n>..<m> mit m >= n. + + + Keine Zellen zum erneuten Ausführen. + + + Der Bereich [{0}..{1}] überschreitet die Länge des Verlaufs ({2}). + + + Führt eine frühere Zelle (oder einen Bereich) als neue Zellen erneut aus. + + + Baut die Kernelsitzung neu auf und löscht alle Variablen und den Laufzeitzustand. + Der Zellenverlauf des Notebooks (bereits getippte Zellen) bleibt erhalten, sodass + .save sie weiterhin erfasst. Vor .reset deklarierte Variablen sind verloren. + + + Kernelzustand zurückgesetzt. + + + Setzt den Kernelzustand zurück; behält den Zellenverlauf. + + + Es wird nach .verso konvertiert; mit --preserve-format bleibt {0} erhalten. + + + Serialisiert das Sitzungs-Notebook. Ohne <path> wird unter dem ursprünglich geladenen + Pfad gespeichert (sofern vorhanden), andernfalls wird ein Fehler gemeldet. Das Format + ergibt sich aus der Endung. Ohne --preserve-format konvertiert ein .save ohne Argument + bei einem aus .ipynb geladenen Notebook in eine gleichnamige .verso-Datei; mit + --preserve-format bleibt das ursprüngliche Format erhalten. + + + {0} nach {1} gespeichert + + + Speichern fehlgeschlagen: {0} + + + Ein Pfad ist erforderlich, wenn in der Sitzung kein Notebook geladen ist. + + + Schreibt das Sitzungs-Notebook auf die Festplatte. + + + Ändert eine der Laufzeiteinstellungen der REPL. + Bekannte Schlüssel: preview.rows, preview.lines, preview.elapsedThresholdMs, confirmOnExit. + + + Mit {0} erhalten Sie die Liste der Schlüssel. + + + Ungültiger Wert true oder false für {0}: '{1}' + + + Ungültige ganze Zahl für {0}: '{1}' + + + Ungültige nicht negative ganze Zahl für {0}: '{1}' + + + Bekannte Schlüssel: {0}. + + + Setzt eine Laufzeiteinstellung der REPL (preview.rows, preview.lines, preview.elapsedThresholdMs). + + + Unbekannter Einstellungsschlüssel '{0}'. + + + Aktives Design: {0} + + + Ohne Argument wird das aktive Design ausgegeben. + Mit einem Namen wird das aktive Design gewechselt. Abgeglichen über DisplayName ohne + Beachtung der Groß-/Kleinschreibung, ersatzweise über ThemeId. + + + Gibt das aktive Design aus oder wechselt es. + + + Zum Design gewechselt: {0} + + + Listet Variablen aus dem gemeinsamen IVariableStore auf. Spalten: Name, Typ, Vorschau. + + + Keine Variablen. + + + Listet Variablen aus dem IVariableStore auf. + + + Gibt die Ausgaben der letzten Zelle (oder von Zelle n, falls angegeben) vollständig + aus und umgeht die Zeilenbegrenzungen der normalen Darstellung. + + + Ungültiger oder außerhalb des Bereichs liegender Zellenindex '{0}'. + + + Zelle [{0}] hat keine Ausgaben. + + + Keine Zellen zum Anzeigen. + + + Gibt die Ausgabe der letzten Zelle ungekürzt aus. + + + Python-Pakete installieren, die eine Zelle importiert, die aber in der Umgebung fehlen. Es werden nur Pakete installiert, deren Distributionsname bekannt ist; alles andere wird gemeldet. + + + Verzeichnis, das nach zusätzlichen Erweiterungsassemblys durchsucht wird. + + + Sprache für Meldungen und Hilfe, eine von: {0}. Standardmäßig die Systemsprache, ersatzweise Englisch. + + + Pfad zum Python-Interpreter, den Python-Zellen verwenden. Überschreibt die automatische Erkennung. + + + Standard: {0} + + + Notebook-Parameter: + + + Ungültige Parameterwerte: + + + Fehlende erforderliche Notebook-Parameter: + + + erforderlich + + + Geben Sie Werte mit --param an, oder verwenden Sie --interactive, um danach gefragt zu werden. + + + Der unbekannte Parameter '{0}' ist in den Notebook-Metadaten nicht definiert. Er wird als Zeichenfolge eingefügt. + + + Parameterdefinitionen werden nur für .verso-Dateien unterstützt. Alle --param-Werte werden als untypisierte Zeichenfolgen eingefügt. + + + Ein Wert ist erforderlich. + + + Fehler: {0} + + + Schwerwiegender Fehler: {0} + + + Warnung: {0} + + + (zurückgeholt: unten kopieren und einfügen oder bearbeiten) + + + <Ausgabe: {0}, {1} Zeichen, binär> + + + Zelle {0} + + + … {0} weitere Zeile + + + … {0} weitere Zeilen + + + (ausgeführt in {0}) + + + Die Ausführung ist fehlgeschlagen. + + + <Bild: {0}, {1}, gespeichert unter {2}> + + + <Bild: {0}, konnte nicht dekodiert werden: {1}> + + + ... ({0} weitere Zeile) + + + ... ({0} weitere Zeilen) + + + … {0} weitere Zeile + + + … {0} weitere Zeilen + + + (keine Parameter) + + + Zellen: {0} insgesamt, {1} erfolgreich, {2} fehlgeschlagen + + + Zusammenfassung + + + Zeit: {0}s + + + <Ausgabe: {0}> + + + [Widget-Ausgabe, wird beim Öffnen des Notebooks angezeigt] + + + Pfad zu einer .verso-, .ipynb- oder .dib-Datei. Ohne Angabe wird mit einem leeren Notebook begonnen. + + + Abgebrochen. + + + {0} Zelle + + + {0} Zellen + + + Eine interaktive Verso-REPL im Terminal starten. + + + Ausführungsfehler: {0} + + + {0} geladen + + + Erweiterungen: + + + Geben Sie {0} für Befehle ein, {1} zum Beenden. + + + Kernel: + + + Notebook: + + + Design: + + + <Standard> + + + <keines> + + + *Entwurf* + + + Fehler im Meta-Befehl '.{0}': {1} + + + Führt zusammen mit <notebook> alle geladenen Zellen aus, bevor die Eingabezeile übernimmt. + + + Pfad zur Verlaufsdatei der Eingabezeile. Mit 'none' wird der dauerhafte Verlauf deaktiviert. + + + Aktiver Kernel für die erste Zelle. Abgeglichen mit ILanguageKernel.KernelId ohne Beachtung der Groß-/Kleinschreibung. Zur Laufzeit mit .kernel änderbar. + + + Standard-Layout-ID, die an .export als ActiveLayoutId übergeben wird, sofern der Befehl sie nicht überschreibt. + + + Verfügbare Kernel ausgeben und beenden. + + + Registrierte Designs ausgeben und beenden. + + + ANSI-Formatierung deaktivieren. Die Ausgabe ist reiner UTF-8-Text. + + + Die zeilenweise Ersatzeingabe erzwingen und PrettyPrompt umgehen, auch wenn das Terminal es unterstützen würde. + + + Ist das geladene Notebook eine .ipynb-Datei, schreibt .save (ohne Argument) nach .ipynb zurück, statt nach .verso zu konvertieren. Zellenausgaben bleiben erhalten. + + + Aktives Design für die Ausgabedarstellung. DisplayName ohne Beachtung der Groß-/Kleinschreibung, ersatzweise ThemeId. + + + Die vorherige Ausführung ist fehlgeschlagen. + + + Verso REPL {0} + + + Geben Sie {0} für die Liste ein. + + + Unbekannter Meta-Befehl '.{0}'. + + + Die Sitzung enthält nicht gespeicherte Zellen. + + + Führen Sie zuerst {0} aus, oder erneut {1}, um zu verwerfen. + + + Verwendung: {0} + + + Verso CLI: Verso-Notebooks ausführen, bereitstellen und konvertieren. + + + Nicht behandelter Fehler: {0} + + + Pfad zu einer .verso-, .ipynb- oder .dib-Datei. + + + [{0}/{1}] Zelle {0} in {2}s abgeschlossen ({3}) + + + Ein Notebook ohne Oberfläche ausführen und Zellenausgaben streamen. + + + [{0}/{1}] Zelle {0} wird ausgeführt ({2})... + + + Ungültige Zellenauswahl '{0}'. Verwenden Sie einen ab null gezählten Index oder eine Zellen-GUID. + + + Ungültiges --param-Format '{0}'. Erwartet wird name=value. + + + Erweiterungen von Drittanbietern werden aus '{0}' geladen. Diese Erweiterungen werden für die Ausführung ohne Oberfläche automatisch zugelassen. + + + Nur die angegebene Zelle ausführen (Index oder GUID). Mehrfach angebbar. + + + Die Ausführung beim ersten fehlgeschlagenen Zellenlauf beenden. + + + Alles, was eine Zelle in die Standardfehlerausgabe schreibt, als Fehler werten. Standardmäßig aus, da Fortschrittsanzeigen, Protokolle und Warnungen dort üblicherweise auch von Programmen geschrieben werden, die erfolgreich laufen. Damit wird eine Pipeline in dieser Hinsicht streng. + + + Die zellenbezogenen Metadaten verso:ui.outputVisibility und verso:ui.inputCollapsed ignorieren; alle Ausgaben vollständig anzeigen. + + + Inhalte von Markdown- und HTML-Zellen in die Terminalausgabe aufnehmen. + + + Fehlende erforderliche Parameter über die Standardeingabe abfragen, statt abzubrechen. + + + Den Standardkernel des Notebooks überschreiben. + + + Ausgabeformat: text, json oder none. + + + Die Ausgabe in eine Datei statt in die Standardausgabe schreiben. Ohne Formatangabe gilt --output json. + + + Einen Notebook-Parameter setzen (Format: name=value). Mehrfach angebbar. + + + Aktualisierte Ausgaben nach der Ausführung in die Notebook-Datei zurückschreiben. + + + Die ermittelten Parameterwerte in der Terminalausgabe anzeigen. + + + Maximale Gesamtausführungszeit in Sekunden. + + + Das Laden von Assemblys erlauben, die in der aktuellen Sitzung erzeugt wurden, ohne Nachfrage. + + + Den Fortschritt der Zellenausführung in die Standardfehlerausgabe schreiben. + + + Die in der Sitzung erzeugte Erweiterung '{0}' wird abgelehnt. Mit --trust-local-assemblies wird sie zugelassen. + + + Nicht unterstütztes oder ungültiges Notebook-Format '{0}'. + + + Optionales Notebook, das beim Start geöffnet wird. + + + Die Verso-Blazor-Anwendung als lokalen Webserver starten. + + + Beim Start keinen Browsertab öffnen. + + + HTTPS deaktivieren (nur HTTP). + + + HTTP-Port, auf dem gelauscht wird. + + + Beim Speichern eines geladenen .ipynb-Notebooks nach .ipynb zurückschreiben, statt nach .verso zu konvertieren. Zellenausgaben bleiben erhalten. + + + Startdetails in die Standardfehlerausgabe schreiben. + + + Drücken Sie Strg+C zum Beenden. + + + Verso läuft unter {0} + + + Der Server konnte nicht gestartet werden: {0} + + + Erweiterungen: {0} + + + Notebook: {0} + + + Befehl + + + Beschreibung + + + Anzeigename + + + Endungen + + + Format + + + ID + + + Art + + + Sprache + + + Name + + + Vorschau + + + Priorität + + + Status + + + Design + + + Typ + + + Version + + \ No newline at end of file diff --git a/src/Verso.Cli/Resources/Strings.es.resx b/src/Verso.Cli/Resources/Strings.es.resx new file mode 100644 index 00000000..6cc357bd --- /dev/null +++ b/src/Verso.Cli/Resources/Strings.es.resx @@ -0,0 +1,851 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Ruta al archivo del cuaderno de origen. + + + Convierte entre formatos de cuaderno. + + + Se convirtió '{0}' en '{1}' + + + Formato '{0}' no admitido. Admitidos: {1} + + + Todavía no se admite la conversión al formato '{0}'. + + + Ruta del archivo de salida. De forma predeterminada, el nombre del archivo de entrada con la nueva extensión. + + + Quita todas las salidas de las celdas del cuaderno convertido. + + + Formato de destino: verso, ipynb, md o dib. + + + El posprocesamiento falló: {0} + + + La serialización falló: {0} + + + Kernels disponibles: {0}. + + + Temas disponibles: {0}. + + + No se pudo deserializar '{0}': {1} + + + El formato de exportación '{0}' no está registrado. + + + No se encontró el archivo de entrada: {0} + + + El kernel '{0}' no está registrado. + + + Varias acciones de exportación comparten el nombre '{0}'. Distíngalas por ActionId: {1}. + + + Varios temas comparten el nombre '{0}'. Distíngalos por ThemeId: {1}. + + + No se encontró el archivo del cuaderno: {0} + + + El tema '{0}' no está registrado. + + + Formato de cuaderno '{0}' no admitido. Formatos admitidos: {1} + + + Formato de salida '{0}' no admitido. Formatos admitidos: {1} + + + La acción de exportación '{0}' produjo un error: {1} + + + Exporta un cuaderno mediante una acción de barra de herramientas de ExportMenu. + + + Se exportó '{0}' a '{1}' + + + La ejecución del cuaderno informó de errores. Se cancela la exportación. + + + --format es obligatorio. + + + <input> es obligatorio salvo que se indique --list. + + + No hay ninguna acción de exportación registrada. + + + La acción de exportación '{0}' no generó ningún archivo. + + + Ejecuta el cuaderno antes de exportarlo para que las salidas almacenadas se actualicen. + + + Formato de exportación, comparado con el DisplayName de un IToolbarAction registrado cuya ubicación sea ExportMenu. No distingue mayúsculas de minúsculas. Entrecomille los valores que contengan espacios. Use --list para ver los formatos instalados. + + + Id del diseño que se aplica durante la exportación, expuesto como ActiveLayoutId en el contexto de la acción. + + + Enumera las acciones de exportación registradas (DisplayName, ActionId, Description) y sale. + + + Enumera los temas registrados (DisplayName, Kind, Description) y sale. + + + Ruta del archivo de salida. Si se omite, el nombre de archivo sugerido por el exportador se escribe en el directorio actual. + + + DisplayName de un tema registrado, comparado sin distinguir mayúsculas de minúsculas. Entrecomille los valores con espacios. Se acepta ThemeId como alternativa para distinguir nombres que coincidan. Use --list-themes para ver los temas instalados. + + + Ejecute '{0}' para ver los detalles. + + + Ejecute '{0}' para ver los formatos disponibles. + + + Muestra la versión de Verso CLI, el runtime y la información de las extensiones. + + + Extensiones: + + + Formateadores: + + + Serializadores: + + + Motor: + + + Runtime: + + + No hay ningún kernel registrado. + + + No hay ningún tema registrado. + + + Borra la pantalla del terminal. El estado de la sesión (variables del kernel, + celdas del cuaderno) se conserva; solo se borra el historial de desplazamiento. + + + Borra el terminal. + + + Serializa el cuaderno de la sesión en <path> usando el serializador cuyas + FileExtensions incluyan la extensión de destino. No cambia la ruta cargada de la + sesión. Resolución idéntica a la de 'verso convert'. + + + Se convirtió el cuaderno de la sesión en {0} ({1}) + + + La conversión falló: {0} + + + Escribe el cuaderno de la sesión en <path> usando el serializador que coincida con su extensión. + + + Sale del REPL. Cuando hay celdas sin guardar, pide confirmación + salvo que confirmOnExit esté deshabilitado en la configuración del usuario. + + + Sale del REPL. + + + Delega en un IToolbarAction registrado con ToolbarPlacement.ExportMenu. + El formato se compara por DisplayName (sin distinguir mayúsculas), con ActionId como alternativa. + El tema se compara por DisplayName (sin distinguir mayúsculas), con ThemeId como alternativa. + Sin --output, escribe el nombre de archivo sugerido por la acción en el directorio actual. + Idéntico a 'verso export'. + + + Se exportó el cuaderno de la sesión a {0} ({1}) + + + Falta el valor de {0}. + + + Exporta el cuaderno de la sesión mediante una acción de barra de herramientas de ExportMenu. + + + Argumento desconocido: {0} + + + Sin argumento, imprime un resumen de todos los metacomandos. + Con un nombre, imprime la ayuda detallada de ese comando. + + + Imprime la ayuda de los metacomandos. + + + Imprime las últimas n celdas enviadas (20 de forma predeterminada). Cada entrada muestra + el contador de entrada y una vista previa de la primera línea no vacía del código. + + + No hay historial. + + + Recuento '{0}' no válido. + + + Imprime los envíos de celdas recientes. + + + Kernel activo: {0} + + + Sin argumento, imprime el kernel activo. + Con un id (LanguageId, comparado sin distinguir mayúsculas), cambia el kernel activo + para las celdas siguientes. Las variables ya declaradas en el kernel anterior siguen + en el ámbito de ese kernel. + + + Imprime o cambia el kernel activo. + + + Se cambió al kernel: {0} ({1}) + + + El precalentamiento del kernel falló: {0} + + + Diseño activo: {0} + + + Diseño borrado. + + + Sin argumento, imprime el id del diseño activo. + Con un id, establece el ActiveLayoutId predeterminado para las llamadas a .export siguientes. + Pase 'none' para borrar el diseño. + + + Diseño establecido en: {0} + + + Imprime o establece el diseño de exportación predeterminado. + + + Donde <kind> es uno de: + kernels, themes, formatters, renderers, serializers, extensions, exporters + Imprime una tabla de los elementos registrados para esa capacidad. + + + No hay ningún elemento registrado. + + + Enumera las capacidades registradas de las extensiones. + + + Tipo de lista '{0}' desconocido. + + + Tipos válidos: {0}. + + + Deserializa el archivo de <path> con el serializador correspondiente y lo instala + como cuaderno de la sesión. Antes pide guardar los cambios pendientes. El estado del + kernel (las variables) se conserva: ejecute .reset primero para empezar de cero. + + + Cargado {0} desde {1} + + + No se pudo cargar: {0} + + + No se encontró el archivo: {0} + + + Carga un cuaderno del disco y reemplaza el cuaderno de la sesión. + + + De un solo uso: el siguiente envío se añade como celda markdown en lugar de celda de código. + Una vez añadida la celda, el REPL vuelve al modo de código. + + + La siguiente celda será markdown. + + + Marca el siguiente envío como celda markdown. + + + Carga el código de la celda n en el búfer del prompt como si se hubiera escrito. + Al pulsar Enter se envía como una celda nueva; la celda original queda intacta. + Los índices fuera de rango producen un error sin vaciar el búfer. + + + El historial contiene {0}. + + + La celda [{0}] está fuera de rango. + + + Se recuperó la celda [{0}]. Edítela y luego pulse Enter y una línea en blanco (o ;;) para enviarla. + + + Carga el código de una celda anterior en el prompt para editarlo. + + + Uso: {0}, donde <n> es un índice de celda contado desde uno, tal como lo muestra {1}. + + + Vuelve a ejecutar la celda n (o el rango n..m, o todas las celdas con 'all') tal cual, + añadiendo cada una como celda nueva. No modifica las celdas anteriores. Un rango envía + las celdas por separado para que cada una muestre sus propias salidas; los errores dentro + de un rango no detienen el resto salvo con --fail-fast. + + + Error de ejecución al volver a ejecutar [{0}]: {1} + + + Índice de celda '{0}' no válido. + + + Rango '{0}' no válido. Se esperaba <n>..<m> con m >= n. + + + No hay celdas que volver a ejecutar. + + + El rango [{0}..{1}] supera la longitud del historial ({2}). + + + Vuelve a ejecutar una celda anterior (o un rango) como celdas nuevas. + + + Reconstruye la sesión del kernel y borra todas las variables y el estado de ejecución. + El historial de celdas del cuaderno (las celdas ya escritas) se conserva, así que .save + las sigue capturando. Las variables declaradas antes de .reset desaparecen. + + + Estado del kernel restablecido. + + + Restablece el estado del kernel; conserva el historial de celdas. + + + Convirtiendo a .verso; use --preserve-format para conservar {0}. + + + Serializa el cuaderno de la sesión. Cuando se omite <path>, guarda en la ruta original + cargada (si la hay) o informa de un error. El formato se deduce de la extensión. + Sin --preserve-format, un .save sin argumento sobre un cuaderno cargado desde .ipynb + se convierte en un archivo .verso hermano; con --preserve-format se conserva el formato original. + + + Guardado {0} en {1} + + + No se pudo guardar: {0} + + + Se necesita una ruta cuando la sesión no tiene ningún cuaderno cargado. + + + Escribe el cuaderno de la sesión en el disco. + + + Actualiza una de las opciones del REPL en tiempo de ejecución. + Claves conocidas: preview.rows, preview.lines, preview.elapsedThresholdMs, confirmOnExit. + + + Pruebe {0} para ver la lista de claves. + + + Valor true o false no válido para {0}: '{1}' + + + Número entero no válido para {0}: '{1}' + + + Número entero no negativo no válido para {0}: '{1}' + + + Claves conocidas: {0}. + + + Establece una opción del REPL en tiempo de ejecución (preview.rows, preview.lines, preview.elapsedThresholdMs). + + + Clave de configuración '{0}' desconocida. + + + Tema activo: {0} + + + Sin argumento, imprime el tema activo. + Con un nombre, cambia el tema activo. Se compara por DisplayName sin distinguir mayúsculas, + con ThemeId como alternativa. + + + Imprime o cambia el tema activo. + + + Se cambió al tema: {0} + + + Enumera las variables del IVariableStore compartido. Columnas: Nombre, Tipo, Vista previa. + + + No hay variables. + + + Enumera las variables del IVariableStore. + + + Imprime al completo las salidas de la última celda (o de la celda n si se indica), + sin aplicar los límites de filas y líneas del renderizado normal. + + + Índice de celda '{0}' no válido o fuera de rango. + + + La celda [{0}] no tiene salidas. + + + No hay celdas que ver. + + + Imprime la salida de la última celda sin recortarla. + + + Instala los paquetes de Python que una celda importa pero el entorno no tiene. Solo se instalan los paquetes cuyo nombre de distribución se conoce; del resto se informa. + + + Directorio en el que buscar ensamblados de extensión adicionales. + + + Idioma de los mensajes y de la ayuda, uno de: {0}. De forma predeterminada, el idioma del sistema, con inglés como alternativa. + + + Ruta al intérprete de Python que usan las celdas de Python. Prevalece sobre la detección automática. + + + predeterminado: {0} + + + Parámetros del cuaderno: + + + Valores de parámetro no válidos: + + + Faltan parámetros obligatorios del cuaderno: + + + obligatorio + + + Indique los valores con --param, o use --interactive para que se le pregunten. + + + El parámetro desconocido '{0}' no está definido en los metadatos del cuaderno. Se inyecta como cadena. + + + Las definiciones de parámetros solo se admiten en los archivos .verso. Se inyectan todos los valores de --param como cadenas sin tipo. + + + Se necesita un valor. + + + Error: {0} + + + Error grave: {0} + + + Advertencia: {0} + + + (recuperada: copie y pegue o edite abajo) + + + <salida: {0}, {1} caracteres, binaria> + + + Celda {0} + + + … {0} línea más + + + … {0} líneas más + + + (ejecutada en {0}) + + + La ejecución falló. + + + <imagen: {0}, {1}, guardada en {2}> + + + <imagen: {0}, no se pudo descodificar: {1}> + + + ... ({0} línea más) + + + ... ({0} líneas más) + + + … {0} fila más + + + … {0} filas más + + + (sin parámetros) + + + Celdas: {0} en total, {1} correctas, {2} con errores + + + Resumen + + + Tiempo: {0}s + + + <salida: {0}> + + + [salida de widget, se muestra al abrir el cuaderno] + + + Ruta a un archivo .verso, .ipynb o .dib. Si se omite, empieza con un cuaderno de borrador vacío. + + + Cancelada. + + + {0} celda + + + {0} celdas + + + Inicia un REPL interactivo de Verso en el terminal. + + + Error de ejecución: {0} + + + {0} cargadas + + + extensiones: + + + Escriba {0} para ver los comandos, {1} para salir. + + + kernel: + + + cuaderno: + + + tema: + + + <predeterminado> + + + <ninguno> + + + *borrador* + + + Error en el metacomando '.{0}': {1} + + + Combinado con <notebook>, ejecuta todas las celdas cargadas antes de ceder el control al prompt. + + + Ruta al archivo de historial del prompt. Use 'none' para deshabilitar el historial persistente. + + + Kernel activo para la primera celda. Se compara con ILanguageKernel.KernelId sin distinguir mayúsculas de minúsculas. Se puede cambiar en tiempo de ejecución con .kernel. + + + Id del diseño predeterminado, que se pasa a .export como ActiveLayoutId salvo que el comando lo sobrescriba. + + + Imprime los kernels disponibles y sale. + + + Imprime los temas registrados y sale. + + + Deshabilita el estilo ANSI. La salida es texto UTF-8 sin formato. + + + Fuerza el prompt alternativo orientado a líneas, sin usar PrettyPrompt aunque el terminal lo admita. + + + Cuando el cuaderno cargado es .ipynb, .save (sin argumento) vuelve a escribir en .ipynb en lugar de convertir a .verso. Las salidas de las celdas se conservan. + + + Tema activo para representar la salida. DisplayName sin distinguir mayúsculas de minúsculas, con ThemeId como alternativa. + + + La ejecución anterior falló. + + + Verso REPL {0} + + + Escriba {0} para ver la lista. + + + Metacomando '.{0}' desconocido. + + + La sesión tiene celdas sin guardar. + + + Ejecute {0} primero, o {1} de nuevo para descartarlas. + + + Uso: {0} + + + Verso CLI: ejecuta, sirve y convierte cuadernos de Verso. + + + Error no controlado: {0} + + + Ruta a un archivo .verso, .ipynb o .dib. + + + [{0}/{1}] La celda {0} terminó en {2}s ({3}) + + + Ejecuta un cuaderno sin interfaz y transmite las salidas de las celdas. + + + [{0}/{1}] Ejecutando la celda {0} ({2})... + + + Selector de celda '{0}' no válido. Use un índice basado en cero o un GUID de celda. + + + Formato de --param '{0}' no válido. Se esperaba name=value. + + + Cargando extensiones de terceros desde '{0}'. Estas extensiones se aprueban automáticamente para la ejecución sin interfaz. + + + Ejecuta solo la celda indicada (índice o GUID). Se puede repetir. + + + Detiene la ejecución en el primer error de celda. + + + Trata como error todo lo que una celda escriba en la salida de error estándar. Deshabilitado de forma predeterminada, porque las barras de progreso, el registro y las advertencias se escriben ahí normalmente desde programas que funcionan bien. Use esta opción para que una canalización sea estricta con ellas. + + + Omite los metadatos verso:ui.outputVisibility y verso:ui.inputCollapsed de cada celda; muestra todas las salidas completas. + + + Incluye el contenido de las celdas markdown y HTML en la salida del terminal. + + + Pide por la entrada estándar los parámetros obligatorios que falten en lugar de fallar. + + + Sobrescribe el kernel predeterminado del cuaderno. + + + Formato de salida: text, json o none. + + + Escribe la salida en un archivo en lugar de en la salida estándar. Implica --output json si no se indica ningún formato. + + + Establece un parámetro del cuaderno (formato: name=value). Se puede repetir. + + + Guarda las salidas actualizadas en el archivo del cuaderno después de la ejecución. + + + Muestra los valores de los parámetros resueltos en la salida del terminal. + + + Tiempo total máximo de ejecución, en segundos. + + + Permite cargar sin consentimiento los ensamblados generados durante la sesión actual. + + + Imprime el progreso de ejecución de las celdas en la salida de error estándar. + + + Se rechaza la extensión '{0}' generada en la sesión. Use --trust-local-assemblies para permitirla. + + + Formato de cuaderno '{0}' no admitido o no válido. + + + Cuaderno opcional que se abre al iniciar. + + + Inicia la aplicación Blazor de Verso como servidor web local. + + + No abre ninguna pestaña del navegador al iniciar. + + + Deshabilita HTTPS (solo HTTP). + + + Puerto HTTP en el que escuchar. + + + Cuando se guarda un cuaderno .ipynb cargado, vuelve a escribir en .ipynb en lugar de convertir a .verso. Las salidas de las celdas se conservan. + + + Imprime los detalles de inicio en la salida de error estándar. + + + Pulse Ctrl+C para detenerlo. + + + Verso se está ejecutando en {0} + + + No se pudo iniciar el servidor: {0} + + + Extensiones: {0} + + + Cuaderno: {0} + + + Comando + + + Descripción + + + Nombre visible + + + Extensiones + + + Formato + + + Id + + + Clase + + + Lenguaje + + + Nombre + + + Vista previa + + + Prioridad + + + Estado + + + Tema + + + Tipo + + + Versión + + \ No newline at end of file diff --git a/src/Verso.Cli/Resources/Strings.ja.resx b/src/Verso.Cli/Resources/Strings.ja.resx new file mode 100644 index 00000000..77bf6636 --- /dev/null +++ b/src/Verso.Cli/Resources/Strings.ja.resx @@ -0,0 +1,852 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 変換元のノートブックファイルのパス。 + + + ノートブックの形式を変換します。 + + + '{0}' を '{1}' に変換しました + + + サポートされていない形式 '{0}' です。使用できる形式: {1} + + + '{0}' 形式への変換はまだサポートされていません。 + + + 出力先のファイルパス。既定では入力のファイル名に新しい拡張子を付けたものになります。 + + + 変換後のノートブックからすべてのセルの出力を取り除きます。 + + + 変換先の形式: verso、ipynb、md、dib のいずれか。 + + + 後処理に失敗しました: {0} + + + シリアル化に失敗しました: {0} + + + 使用できるカーネル: {0}。 + + + 使用できるテーマ: {0}。 + + + '{0}' を読み込めませんでした: {1} + + + エクスポート形式 '{0}' は登録されていません。 + + + 入力ファイルが見つかりません: {0} + + + カーネル '{0}' は登録されていません。 + + + 複数のエクスポートアクションが表示名 '{0}' を共有しています。ActionId で指定してください: {1}。 + + + 複数のテーマが表示名 '{0}' を共有しています。ThemeId で指定してください: {1}。 + + + ノートブックファイルが見つかりません: {0} + + + テーマ '{0}' は登録されていません。 + + + サポートされていないノートブック形式 '{0}' です。使用できる形式: {1} + + + サポートされていない出力形式 '{0}' です。使用できる形式: {1} + + + エクスポートアクション '{0}' で例外が発生しました: {1} + + + ExportMenu のツールバーアクションを使ってノートブックをエクスポートします。 + + + '{0}' を '{1}' にエクスポートしました + + + ノートブックの実行でエラーが報告されました。エクスポートを中止します。 + + + --format は必須です。 + + + --list を指定しない場合、<input> は必須です。 + + + エクスポートアクションは登録されていません。 + + + エクスポートアクション '{0}' はファイルを生成しませんでした。 + + + エクスポートの前にノートブックを実行し、保存されている出力を更新します。 + + + エクスポート形式。配置が ExportMenu である登録済み IToolbarAction の DisplayName と照合します。大文字と小文字は区別しません。空白を含む値は引用符で囲んでください。インストール済みの形式は --list で確認できます。 + + + エクスポート中に適用するレイアウトの id。アクションのコンテキストでは ActiveLayoutId として参照できます。 + + + 登録済みのエクスポートアクション (DisplayName、ActionId、Description) を一覧表示して終了します。 + + + 登録済みのテーマ (DisplayName、Kind、Description) を一覧表示して終了します。 + + + 出力先のファイルパス。省略した場合は、エクスポーターが提案するファイル名で現在のディレクトリに書き出します。 + + + 登録済みテーマの DisplayName。大文字と小文字は区別しません。空白を含む値は引用符で囲んでください。表示名が重複する場合は ThemeId も指定できます。インストール済みのテーマは --list-themes で確認できます。 + + + 詳しくは '{0}' を実行してください。 + + + 使用できる形式は '{0}' で確認できます。 + + + Verso CLI のバージョン、ランタイム、拡張機能の情報を表示します。 + + + 拡張機能: + + + フォーマッター: + + + シリアライザー: + + + エンジン: + + + ランタイム: + + + カーネルは登録されていません。 + + + テーマは登録されていません。 + + + ターミナルの画面を消去します。セッションの状態 (カーネルの変数、ノートブックのセル) + は保持され、スクロールバックだけが消去されます。 + + + ターミナルを消去します。 + + + 現在のセッションのノートブックを、対象の拡張子を FileExtensions に含む + シリアライザーで <path> に書き出します。セッションが読み込んでいるパスは + 変わりません。解決方法は 'verso convert' と同じです。 + + + セッションのノートブックを {0} に変換しました ({1}) + + + 変換に失敗しました: {0} + + + 拡張子に対応するシリアライザーで、セッションのノートブックを <path> に書き出します。 + + + REPL を終了します。未保存のセルがある場合は確認を求めます。 + ユーザー設定で confirmOnExit を無効にしている場合は求めません。 + + + REPL を終了します。 + + + ToolbarPlacement.ExportMenu で登録された IToolbarAction に処理を渡します。 + 形式は DisplayName (大文字と小文字を区別しない) で照合し、ActionId を代替とします。 + テーマは DisplayName (大文字と小文字を区別しない) で照合し、ThemeId を代替とします。 + --output を指定しない場合は、アクションが提案するファイル名で現在のディレクトリに書き出します。 + 'verso export' と同じです。 + + + セッションのノートブックを {0} にエクスポートしました ({1}) + + + {0} の値がありません。 + + + ExportMenu のツールバーアクションで、セッションのノートブックをエクスポートします。 + + + 不明な引数です: {0} + + + 引数なしで実行すると、すべてのメタコマンドの概要を表示します。 + 名前を指定すると、そのコマンドの詳しいヘルプを表示します。 + + + メタコマンドのヘルプを表示します。 + + + 直近 n 件の実行済みセルを表示します (既定は 20)。各行には入力番号と、 + ソースの空でない最初の行のプレビューを表示します。 + + + 履歴はありません。 + + + 件数 '{0}' が正しくありません。 + + + 最近実行したセルを表示します。 + + + アクティブなカーネル: {0} + + + 引数なしで実行すると、アクティブなカーネルを表示します。 + id (LanguageId、大文字と小文字を区別しない) を指定すると、以降のセルの + アクティブなカーネルを切り替えます。切り替え前のカーネルで宣言済みの変数は、 + そのカーネルのスコープに残ります。 + + + アクティブなカーネルを表示または切り替えます。 + + + カーネルを切り替えました: {0} ({1}) + + + カーネルの事前起動に失敗しました: {0} + + + アクティブなレイアウト: {0} + + + レイアウトを解除しました。 + + + 引数なしで実行すると、アクティブなレイアウトの id を表示します。 + id を指定すると、以降の .export で使う既定の ActiveLayoutId を設定します。 + 'none' を渡すとレイアウトを解除します。 + + + レイアウトを設定しました: {0} + + + エクスポートの既定レイアウトを表示または設定します。 + + + <kind> は次のいずれかです: + kernels, themes, formatters, renderers, serializers, extensions, exporters + その種類について登録されている項目を表で表示します。 + + + 登録されている項目はありません。 + + + 登録されている拡張機能の機能を一覧表示します。 + + + 不明な種類 '{0}' です。 + + + 指定できる種類: {0}。 + + + <path> のファイルを対応するシリアライザーで読み込み、セッションの + ノートブックとして設定します。未保存の変更があれば先に保存を求めます。 + カーネルの状態 (変数) は保持されます。まっさらな状態から始めるには、先に .reset を実行してください。 + + + {1} から {0} を読み込みました + + + 読み込めませんでした: {0} + + + ファイルが見つかりません: {0} + + + ディスクからノートブックを読み込み、セッションのノートブックを置き換えます。 + + + 1 回かぎりの指定です。次の入力はコードセルではなく Markdown セルとして追加されます。 + そのセルが追加されると、REPL はコードモードに戻ります。 + + + 次のセルは Markdown になります。 + + + 次の入力を Markdown セルとして扱います。 + + + セル n のソースを、入力されたかのようにプロンプトのバッファーに読み込みます。 + Enter を押すと新しいセルとして実行され、元のセルはそのまま残ります。 + 範囲外の番号を指定した場合は、バッファーを消さずにエラーを表示します。 + + + 履歴には {0} があります。 + + + セル [{0}] は範囲外です。 + + + セル [{0}] を呼び出しました。編集したら Enter を押し、空行 (または ;;) を入力して実行してください。 + + + 以前のセルのソースを、編集できるようプロンプトに読み込みます。 + + + 使い方: {0}。<n> は 1 から数えたセルの番号で、{1} で確認できます。 + + + セル n (範囲 n..m、または 'all' ですべてのセル) をそのまま再実行し、 + それぞれを新しいセルとして追加します。元のセルは変更しません。範囲を指定した + 場合はセルを 1 つずつ実行するので、それぞれが自分の出力を表示します。範囲内で + 失敗しても、--fail-fast を指定しないかぎり残りは実行されます。 + + + 再実行 [{0}] で実行エラーが発生しました: {1} + + + セル番号 '{0}' が正しくありません。 + + + 範囲 '{0}' が正しくありません。<n>..<m> の形式で、m >= n としてください。 + + + 再実行するセルがありません。 + + + 範囲 [{0}..{1}] は履歴の長さ ({2}) を超えています。 + + + 以前のセル (または範囲) を新しいセルとして再実行します。 + + + カーネルのセッションを作り直し、すべての変数と実行時の状態を消去します。 + ノートブックのセル履歴 (入力済みのセル) は保持されるので、.save でそのまま + 保存できます。.reset より前に宣言した変数は失われます。 + + + カーネルの状態をリセットしました。 + + + カーネルの状態をリセットします。セル履歴は保持されます。 + + + .verso に変換します。{0} のまま保存するには --preserve-format を指定してください。 + + + セッションのノートブックを書き出します。<path> を省略した場合は、読み込み元の + パスがあればそこに保存し、なければエラーを表示します。形式は拡張子から判断します。 + --preserve-format を指定しない場合、.ipynb から読み込んだノートブックに引数なしの + .save を実行すると、同じ場所の .verso ファイルに変換されます。--preserve-format を + 指定すると元の形式のまま保存します。 + + + {0} を {1} に保存しました + + + 保存できませんでした: {0} + + + セッションがノートブックを読み込んでいない場合は、パスの指定が必要です。 + + + セッションのノートブックをディスクに書き出します。 + + + REPL の実行時設定を更新します。 + 指定できるキー: preview.rows、preview.lines、preview.elapsedThresholdMs、confirmOnExit。 + + + キーの一覧は {0} で確認できます。 + + + {0} の true または false の値が正しくありません: '{1}' + + + {0} の整数が正しくありません: '{1}' + + + {0} の 0 以上の整数が正しくありません: '{1}' + + + 指定できるキー: {0}。 + + + REPL の実行時設定を変更します (preview.rows、preview.lines、preview.elapsedThresholdMs)。 + + + 不明な設定キー '{0}' です。 + + + アクティブなテーマ: {0} + + + 引数なしで実行すると、アクティブなテーマを表示します。 + 名前を指定すると、アクティブなテーマを変更します。DisplayName を大文字と小文字を + 区別せずに照合し、ThemeId を代替とします。 + + + アクティブなテーマを表示または切り替えます。 + + + テーマを切り替えました: {0} + + + 共有の IVariableStore にある変数を一覧表示します。列: 名前、型、プレビュー。 + + + 変数はありません。 + + + IVariableStore の変数を一覧表示します。 + + + 最後のセル (n を指定した場合はそのセル) の出力を、通常の表示で適用される + 行数の上限を無視して全体表示します。 + + + セル番号 '{0}' が正しくないか、範囲外です。 + + + セル [{0}] に出力はありません。 + + + 表示するセルがありません。 + + + 最後のセルの出力を省略せずに表示します。 + + + セルが import していて環境にない Python パッケージをインストールします。配布名がわかっているパッケージだけをインストールし、それ以外は報告します。 + + + 追加の拡張機能アセンブリを探すディレクトリ。 + + + メッセージとヘルプの言語。{0} のいずれか。既定はシステムの言語で、対応していない場合は英語になります。 + + + Python セルが使うインタープリターのパス。自動検出より優先されます。 + + + 既定: {0} + + + ノートブックのパラメーター: + + + パラメーターの値が正しくありません: + + + 必須のノートブックパラメーターが指定されていません: + + + 必須 + + + --param で値を指定するか、--interactive で入力を求めるようにしてください。 + + + パラメーター '{0}' はノートブックのメタデータで定義されていません。文字列として渡します。 + + + パラメーターの定義は .verso ファイルでのみサポートされています。--param の値はすべて型のない文字列として渡します。 + + + 値の入力が必要です。 + + + エラー: {0} + + + 致命的なエラー: {0} + + + 警告: {0} + + + (呼び出しました: 下にコピーするか、編集してください) + + + <出力: {0}、{1} 文字、バイナリ> + + + セル {0} + + + … 他 {0} 行 + + + … 他 {0} 行 + + + ({0} で実行) + + + 実行に失敗しました。 + + + <画像: {0}、{1}、{2} に保存> + + + <画像: {0}、デコードに失敗: {1}> + + + ... (他 {0} 行) + + + ... (他 {0} 行) + + + … 他 {0} 行 + + + … 他 {0} 行 + + + (パラメーターなし) + + + セル: 合計 {0} 件、成功 {1} 件、失敗 {2} 件 + + + 概要 + + + 時間: {0} 秒 + + + <出力: {0}> + + + [ウィジェットの出力。ノートブックを開くと表示されます] + + + .verso、.ipynb、.dib のいずれかのファイルのパス。省略した場合は、空のスクラッチノートブックで開始します。 + + + 中止しました。 + + + {0} 個のセル + + + {0} 個のセル + + + ターミナルで対話型の Verso REPL を開始します。 + + + 実行エラー: {0} + + + {0} 個読み込み済み + + + 拡張機能: + + + コマンドの一覧は {0}、終了は {1} と入力してください。 + + + カーネル: + + + ノートブック: + + + テーマ: + + + <既定> + + + <なし> + + + *スクラッチ* + + + メタコマンド '.{0}' でエラーが発生しました: {1} + + + <notebook> と併せて指定すると、プロンプトに制御を渡す前に読み込んだセルをすべて実行します。 + + + プロンプトの履歴ファイルのパス。'none' を指定すると履歴を保存しません。 + + + 最初のセルで使うカーネル。ILanguageKernel.KernelId と大文字小文字を区別せずに照合します。実行中に .kernel で変更できます。 + + + 既定のレイアウト id。コマンドで上書きしないかぎり、ActiveLayoutId として .export に渡されます。 + + + 使用できるカーネルを表示して終了します。 + + + 登録済みのテーマを表示して終了します。 + + + ANSI の装飾を無効にします。出力はプレーンな UTF-8 テキストになります。 + + + ターミナルが対応している場合でも PrettyPrompt を使わず、行単位のプロンプトを使います。 + + + 読み込んだノートブックが .ipynb の場合、引数なしの .save で .verso に変換せず .ipynb に書き戻します。セルの出力は保持されます。 + + + 出力の表示に使うテーマ。DisplayName と大文字小文字を区別せずに照合し、ThemeId を代替とします。 + + + 前回の実行に失敗しています。 + + + Verso REPL {0} + + + 一覧は {0} と入力してください。 + + + 不明なメタコマンド '.{0}' です。 + + + セッションに未保存のセルがあります。 + + + 先に {0} を実行するか、もう一度 {1} を実行すると破棄されます。 + + + 使い方: {0} + + + Verso CLI: Verso ノートブックの実行、配信、変換を行います。 + + + 処理されなかったエラー: {0} + + + .verso、.ipynb、.dib のいずれかのファイルのパス。 + + + [{0}/{1}] セル {0} が {2} 秒で完了しました ({3}) + + + ノートブックをヘッドレスで実行し、セルの出力を順に表示します。 + + + [{0}/{1}] セル {0} を実行しています ({2})... + + + セルの指定 '{0}' が正しくありません。0 から数えた番号かセルの GUID を使ってください。 + + + --param の形式 '{0}' が正しくありません。name=value の形で指定してください。 + + + '{0}' からサードパーティの拡張機能を読み込みます。ヘッドレス実行では、これらの拡張機能は自動的に許可されます。 + + + 指定したセルだけを実行します (番号または GUID)。繰り返し指定できます。 + + + 最初にセルが失敗した時点で実行を止めます。 + + + セルが標準エラーに書き込んだ内容をすべて失敗として扱います。既定では無効です。進捗バーやログ、警告は、正常に動作しているプログラムでも標準エラーに書き込まれるためです。パイプラインでこれらを厳密に扱いたい場合に指定してください。 + + + セルごとの verso:ui.outputVisibility と verso:ui.inputCollapsed メタデータを無視し、すべての出力を省略せずに表示します。 + + + Markdown と HTML のセルの内容をターミナルの出力に含めます。 + + + 必須パラメーターが指定されていない場合、失敗させずに標準入力から入力を求めます。 + + + ノートブックの既定のカーネルを上書きします。 + + + 出力形式: text、json、none のいずれか。 + + + 標準出力ではなくファイルに出力します。形式を指定しない場合は --output json とみなされます。 + + + ノートブックのパラメーターを設定します (形式: name=value)。繰り返し指定できます。 + + + 実行後、更新された出力をノートブックファイルに保存します。 + + + 解決されたパラメーターの値をターミナルの出力に表示します。 + + + 実行全体の最大時間 (秒)。 + + + 現在のセッション中に生成されたアセンブリを、許可を求めずに読み込めるようにします。 + + + セルの実行状況を標準エラーに表示します。 + + + セッション中に生成された拡張機能 '{0}' の読み込みを拒否しました。許可するには --trust-local-assemblies を指定してください。 + + + ノートブック形式 '{0}' はサポートされていないか、正しくありません。 + + + 起動時に開くノートブック (省略可)。 + + + Verso の Blazor アプリケーションをローカルの Web サーバーとして起動します。 + + + 起動時にブラウザーのタブを開きません。 + + + HTTPS を無効にします (HTTP のみ)。 + + + 待ち受ける HTTP ポート。 + + + 読み込んだ .ipynb ノートブックを保存するとき、.verso に変換せず .ipynb に書き戻します。セルの出力は保持されます。 + + + 起動時の詳細を標準エラーに表示します。 + + + 停止するには Ctrl+C を押してください。 + + + Verso は {0} で動作しています + + + サーバーを起動できませんでした: {0} + + + 拡張機能: {0} + + + ノートブック: {0} + + + コマンド + + + 説明 + + + 表示名 + + + 拡張子 + + + 形式 + + + ID + + + 種類 + + + 言語 + + + 名前 + + + プレビュー + + + 優先度 + + + 状態 + + + テーマ + + + + + + バージョン + + \ No newline at end of file diff --git a/src/Verso.Cli/Resources/Strings.qps-Ploc.resx b/src/Verso.Cli/Resources/Strings.qps-Ploc.resx new file mode 100644 index 00000000..aa96a3a1 --- /dev/null +++ b/src/Verso.Cli/Resources/Strings.qps-Ploc.resx @@ -0,0 +1,851 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + [!!Pàth tò thé šòùrçé ñòtébòòk fïlé.···!!] + + + [!!Çòñvért bétwééñ ñòtébòòk fòrmàtš.···!!] + + + [!!Çòñvértéd '{0}' tò '{1}'···!!] + + + [!!Ùñšùppòrtéd fòrmàt '{0}'. Šùppòrtéd: {1}···!!] + + + [!!Çòñvértïñg tò '{0}' fòrmàt ïš ñòt ÿét šùppòrtéd.···!!] + + + [!!Òùtpùt fïlé pàth. Défàùltš tò ïñpùt fïléñàmé wïth thé ñéw éxtéñšïòñ.···!!] + + + [!!Rémòvé àll çéll òùtpùtš fròm thé çòñvértéd ñòtébòòk.···!!] + + + [!!Tàrgét fòrmàt: véršò, ïpÿñb, md, òr dïb.···!!] + + + [!!Pòšt-pròçéššïñg fàïléd: {0}···!!] + + + [!!Šérïàlïzàtïòñ fàïléd: {0}···!!] + + + [!!Àvàïlàblé kérñélš: {0}.···!!] + + + [!!Àvàïlàblé théméš: {0}.···!!] + + + [!!Fàïléd tò déšérïàlïzé '{0}': {1}···!!] + + + [!!Éxpòrt fòrmàt '{0}' ïš ñòt régïštéréd.···!!] + + + [!!Ïñpùt fïlé ñòt fòùñd: {0}···!!] + + + [!!Kérñél '{0}' ïš ñòt régïštéréd.···!!] + + + [!!Mùltïplé éxpòrt àçtïòñš šhàré dïšplàÿ ñàmé '{0}'. Dïšàmbïgùàté bÿ ÀçtïòñÏd: {1}.···!!] + + + [!!Mùltïplé théméš šhàré dïšplàÿ ñàmé '{0}'. Dïšàmbïgùàté bÿ ThéméÏd: {1}.···!!] + + + [!!Ñòtébòòk fïlé ñòt fòùñd: {0}···!!] + + + [!!Thémé '{0}' ïš ñòt régïštéréd.···!!] + + + [!!Ùñšùppòrtéd ñòtébòòk fòrmàt '{0}'. Šùppòrtéd fòrmàtš: {1}···!!] + + + [!!Ùñšùppòrtéd òùtpùt fòrmàt '{0}'. Šùppòrtéd fòrmàtš: {1}···!!] + + + [!!Éxpòrt àçtïòñ '{0}' thréw: {1}···!!] + + + [!!Éxpòrt à ñòtébòòk vïà àñ ÉxpòrtMéñù tòòlbàr àçtïòñ.···!!] + + + [!!Éxpòrtéd '{0}' tò '{1}'···!!] + + + [!!Ñòtébòòk éxéçùtïòñ répòrtéd érròrš. Àbòrtïñg éxpòrt.···!!] + + + [!!--fòrmàt ïš réqùïréd.···!!] + + + [!!<ïñpùt> ïš réqùïréd ùñléšš --lïšt ïš špéçïfïéd.···!!] + + + [!!Ñò éxpòrt àçtïòñš àré régïštéréd.···!!] + + + [!!Éxpòrt àçtïòñ '{0}' dïd ñòt pròdùçé à fïlé.···!!] + + + [!!Éxéçùté thé ñòtébòòk béfòré éxpòrtïñg šò štòréd òùtpùtš àré réfréšhéd.···!!] + + + [!!Éxpòrt fòrmàt, màtçhéd àgàïñšt thé DïšplàÿÑàmé òf à régïštéréd ÏTòòlbàrÀçtïòñ whòšé plàçéméñt ïš ÉxpòrtMéñù. Çàšé-ïñšéñšïtïvé. Qùòté vàlùéš çòñtàïñïñg whïtéšpàçé. Ùšé --lïšt tò šéé ïñštàlléd fòrmàtš.···!!] + + + [!!Làÿòùt ïd tò àpplÿ dùrïñg éxpòrt, éxpòšéd àš ÀçtïvéLàÿòùtÏd òñ thé àçtïòñ çòñtéxt.···!!] + + + [!!Lïšt régïštéréd éxpòrt àçtïòñš (DïšplàÿÑàmé, ÀçtïòñÏd, Déšçrïptïòñ) àñd éxït.···!!] + + + [!!Lïšt régïštéréd théméš (DïšplàÿÑàmé, Kïñd, Déšçrïptïòñ) àñd éxït.···!!] + + + [!!Òùtpùt fïlé pàth. Ïf òmïttéd, thé éxpòrtér'š šùggéštéd fïléñàmé ïš wrïttéñ tò thé çùrréñt dïréçtòrÿ.···!!] + + + [!!DïšplàÿÑàmé òf à régïštéréd thémé, màtçhéd çàšé-ïñšéñšïtïvélÿ. Qùòté vàlùéš wïth whïtéšpàçé. ThéméÏd ïš àççéptéd àš à fàllbàçk tò dïšàmbïgùàté dïšplàÿ-ñàmé çòllïšïòñš. Ùšé --lïšt-théméš tò šéé ïñštàlléd théméš.···!!] + + + [!!Rùñ '{0}' fòr détàïlš.···!!] + + + [!!Rùñ '{0}' tò šéé àvàïlàblé fòrmàtš.···!!] + + + [!!Dïšplàÿ Véršò ÇLÏ véršïòñ, rùñtïmé, àñd éxtéñšïòñ ïñfòrmàtïòñ.···!!] + + + [!!Éxtéñšïòñš:···!!] + + + [!!Fòrmàttérš:···!!] + + + [!!Šérïàlïzérš:···!!] + + + [!!Éñgïñé:···!!] + + + [!!Rùñtïmé:···!!] + + + [!!Ñò kérñélš àré régïštéréd.···!!] + + + [!!Ñò théméš àré régïštéréd.···!!] + + + [!! Çléàrš thé térmïñàl šçrééñ. Šéššïòñ štàté (kérñél vàrïàbléš, ñòtébòòk çéllš) + ïš préšérvéd; òñlÿ thé šçròllbàçk ïš çléàréd.···!!] + + + [!!Çléàrš thé térmïñàl.···!!] + + + [!! Šérïàlïzéš thé çùrréñt šéššïòñ ñòtébòòk tò <pàth> ùšïñg thé šérïàlïzér whòšé + FïléÉxtéñšïòñš ïñçlùdé thé tàrgét éxtéñšïòñ. Dòéš ñòt çhàñgé thé šéššïòñ'š + lòàdéd pàth. Ïdéñtïçàl réšòlùtïòñ tò 'véršò çòñvért'.···!!] + + + [!!Çòñvértéd thé šéššïòñ ñòtébòòk tò {0} ({1})···!!] + + + [!!Çòñvért fàïléd: {0}···!!] + + + [!!Wrïtéš thé šéššïòñ ñòtébòòk tò <pàth> ùšïñg thé šérïàlïzér màtçhïñg ïtš éxtéñšïòñ.···!!] + + + [!! Éxïtš thé RÉPL. Whéñ ùñšàvéd çéllš éxïšt, pròmptš fòr çòñfïrmàtïòñ + ùñléšš çòñfïrmÒñÉxït ïš dïšàbléd ïñ ùšér šéttïñgš.···!!] + + + [!!Éxïtš thé RÉPL.···!!] + + + [!! Dïšpàtçhéš tò àñ ÏTòòlbàrÀçtïòñ régïštéréd wïth TòòlbàrPlàçéméñt.ÉxpòrtMéñù. + Fòrmàt ïš màtçhéd bÿ DïšplàÿÑàmé (çàšé-ïñšéñšïtïvé), ÀçtïòñÏd àš fàllbàçk. + Thémé ïš màtçhéd bÿ DïšplàÿÑàmé (çàšé-ïñšéñšïtïvé), ThéméÏd àš fàllbàçk. + Wïthòùt --òùtpùt, wrïtéš thé àçtïòñ'š šùggéštéd fïléñàmé tò thé çùrréñt dïréçtòrÿ. + Ïdéñtïçàl tò 'véršò éxpòrt'.···!!] + + + [!!Éxpòrtéd thé šéššïòñ ñòtébòòk tò {0} ({1})···!!] + + + [!!Mïššïñg vàlùé fòr {0}.···!!] + + + [!!Éxpòrtš thé šéššïòñ ñòtébòòk vïà àñ ÉxpòrtMéñù tòòlbàr àçtïòñ.···!!] + + + [!!Ùñkñòwñ àrgùméñt: {0}···!!] + + + [!! Wïth ñò àrgùméñt, prïñtš àñ òvérvïéw òf àll métà-çòmmàñdš. + Wïth à ñàmé, prïñtš détàïléd hélp fòr thàt çòmmàñd.···!!] + + + [!!Prïñtš métà-çòmmàñd hélp.···!!] + + + [!! Prïñtš thé làšt ñ šùbmïttéd çéllš (défàùlt 20). Éàçh éñtrÿ šhòwš thé ïñpùt çòùñtér + àñd à prévïéw òf thé fïršt ñòñ-émptÿ lïñé òf šòùrçé.···!!] + + + [!!Ñò hïštòrÿ.···!!] + + + [!!Ïñvàlïd çòùñt '{0}'.···!!] + + + [!!Prïñtš réçéñt çéll šùbmïššïòñš.···!!] + + + [!!Àçtïvé kérñél: {0}···!!] + + + [!! Wïth ñò àrgùméñt, prïñtš thé àçtïvé kérñél. + Wïth àñ ïd (LàñgùàgéÏd, màtçhéd çàšé-ïñšéñšïtïvélÿ), šwïtçhéš thé àçtïvé kérñél + fòr šùbšéqùéñt çéllš. Vàrïàbléš àlréàdÿ déçlàréd ïñ thé prïòr kérñél rémàïñ ïñ + thàt kérñél'š šçòpé.···!!] + + + [!!Prïñtš òr šwïtçhéš thé àçtïvé kérñél.···!!] + + + [!!Šwïtçhéd tò kérñél: {0} ({1})···!!] + + + [!!Kérñél wàrm-ùp fàïléd: {0}···!!] + + + [!!Àçtïvé làÿòùt: {0}···!!] + + + [!!Làÿòùt çléàréd.···!!] + + + [!! Wïth ñò àrgùméñt, prïñtš thé àçtïvé làÿòùt ïd. + Wïth àñ ïd, šétš thé défàùlt ÀçtïvéLàÿòùtÏd fòr šùbšéqùéñt .éxpòrt çàllš. + Pàšš 'ñòñé' tò çléàr thé làÿòùt.···!!] + + + [!!Làÿòùt šét tò: {0}···!!] + + + [!!Prïñtš òr šétš thé défàùlt éxpòrt làÿòùt.···!!] + + + [!! Whéré <kïñd> ïš òñé òf: + kérñélš, théméš, fòrmàttérš, réñdérérš, šérïàlïzérš, éxtéñšïòñš, éxpòrtérš + Prïñtš à tàblé òf thé régïštéréd ïtémš fòr thàt çàpàbïlïtÿ.···!!] + + + [!!Ñò ïtémš régïštéréd.···!!] + + + [!!Lïštš régïštéréd éxtéñšïòñ çàpàbïlïtïéš.···!!] + + + [!!Ùñkñòwñ lïšt kïñd '{0}'.···!!] + + + [!!Vàlïd kïñdš: {0}.···!!] + + + [!! Déšérïàlïzéš thé fïlé àt <pàth> thròùgh thé màtçhïñg šérïàlïzér àñd ïñštàllš ït + àš thé šéššïòñ ñòtébòòk. Pròmptš tò šàvé ùñšàvéd çhàñgéš fïršt. Kérñél štàté + (vàrïàbléš) ïš préšérvéd: rùñ .réšét fïršt fòr à çléàñ štàrt.···!!] + + + [!!Lòàdéd {0} fròm {1}···!!] + + + [!!Fàïléd tò lòàd: {0}···!!] + + + [!!Fïlé ñòt fòùñd: {0}···!!] + + + [!!Lòàdš à ñòtébòòk fròm dïšk, réplàçïñg thé šéššïòñ ñòtébòòk.···!!] + + + [!! Òñé-šhòt: thé ñéxt šùbmïššïòñ ïš àppéñdéd àš à màrkdòwñ çéll ïñštéàd òf à çòdé çéll. + Àftér thé çéll ïš àppéñdéd, thé RÉPL révértš tò çòdé mòdé.···!!] + + + [!!Thé ñéxt çéll wïll bé màrkdòwñ.···!!] + + + [!!Màrkš thé ñéxt šùbmïššïòñ àš à màrkdòwñ çéll.···!!] + + + [!! Lòàdš çéll ñ'š šòùrçé ïñtò thé pròmpt bùffér àš ïf thé ùšér hàd tÿpéd ït. + Préššïñg Éñtér šùbmïtš ït àš à ñéw çéll; thé òrïgïñàl çéll rémàïñš ùñtòùçhéd. + Òùt-òf-ràñgé ïñdïçéš pròdùçé àñ érròr wïthòùt çléàrïñg thé bùffér.···!!] + + + [!!Hïštòrÿ çòñtàïñš {0}.···!!] + + + [!!Çéll [{0}] ïš òùt òf ràñgé.···!!] + + + [!!Réçàlléd çéll [{0}]. Édït ït, théñ préšš Éñtér àñd à blàñk lïñé (òr ;;) tò šùbmït.···!!] + + + [!!Lòàdš à prïòr çéll'š šòùrçé ïñtò thé pròmpt fòr édïtïñg.···!!] + + + [!!Ùšàgé: {0}, whéré <ñ> ïš à çéll ïñdéx çòùñtéd fròm òñé, àš šhòwñ bÿ {1}.···!!] + + + [!! Ré-éxéçùtéš çéll ñ (òr ràñgé ñ..m, òr évérÿ çéll wïth 'àll') vérbàtïm, + àppéñdïñg éàçh àš à ñéw çéll. Dòéš ñòt mùtàté prïòr çéllš. À ràñgé šùbmïtš + çéllš ïñdïvïdùàllÿ šò éàçh réñdérš ïtš òwñ òùtpùtš; fàïlùréš wïthïñ à ràñgé + dò ñòt štòp thé réšt ùñléšš --fàïl-fàšt.···!!] + + + [!!Éxéçùtïòñ érròr ïñ rérùñ [{0}]: {1}···!!] + + + [!!Ïñvàlïd çéll ïñdéx '{0}'.···!!] + + + [!!Ïñvàlïd ràñgé '{0}'. Éxpéçtéd <ñ>..<m> wïth m >= ñ.···!!] + + + [!!Ñò çéllš tò rérùñ.···!!] + + + [!!Ràñgé [{0}..{1}] éxçéédš thé hïštòrÿ léñgth ({2}).···!!] + + + [!!Ré-éxéçùtéš à prïòr çéll (òr à ràñgé) àš ñéw çéllš.···!!] + + + [!! Rébùïldš thé kérñél šéššïòñ, çléàrïñg àll vàrïàbléš àñd rùñtïmé štàté. + Thé ñòtébòòk'š çéll hïštòrÿ (çéllš àlréàdÿ tÿpéd) ïš préšérvéd, šò .šàvé + štïll çàptùréš thém. Vàrïàbléš déçlàréd béfòré .réšét àré gòñé.···!!] + + + [!!Kérñél štàté réšét.···!!] + + + [!!Réšétš kérñél štàté; kéépš çéll hïštòrÿ.···!!] + + + [!!Çòñvértïñg tò .véršò; ùšé --préšérvé-fòrmàt tò kéép {0}.···!!] + + + [!! Šérïàlïzéš thé šéššïòñ ñòtébòòk. Whéñ <pàth> ïš òmïttéd, šàvéš tò thé òrïgïñàl + lòàdéd pàth (ïf àñÿ) òr répòrtš àñ érròr. Fòrmàt ïš ïñférréd fròm thé éxtéñšïòñ. + Wïthòùt --préšérvé-fòrmàt, à .šàvé wïth ñò àrg àgàïñšt àñ .ïpÿñb-lòàdéd ñòtébòòk + çòñvértš tò à šïblïñg .véršò fïlé; wïth --préšérvé-fòrmàt thé òrïgïñàl fòrmàt ïš képt.···!!] + + + [!!Šàvéd {0} tò {1}···!!] + + + [!!Fàïléd tò šàvé: {0}···!!] + + + [!!À pàth ïš réqùïréd whéñ thé šéššïòñ hàš ñò lòàdéd ñòtébòòk.···!!] + + + [!!Wrïtéš thé šéššïòñ ñòtébòòk tò dïšk.···!!] + + + [!! Ùpdàtéš òñé òf thé rùñtïmé RÉPL šéttïñgš. + Kñòwñ kéÿš: prévïéw.ròwš, prévïéw.lïñéš, prévïéw.élàpšédThréšhòldMš, çòñfïrmÒñÉxït.···!!] + + + [!!Trÿ {0} fòr thé lïšt òf kéÿš.···!!] + + + [!!Ïñvàlïd trùé òr fàlšé vàlùé fòr {0}: '{1}'···!!] + + + [!!Ïñvàlïd whòlé ñùmbér fòr {0}: '{1}'···!!] + + + [!!Ïñvàlïd ñòñ-ñégàtïvé whòlé ñùmbér fòr {0}: '{1}'···!!] + + + [!!Kñòwñ kéÿš: {0}.···!!] + + + [!!Šétš à rùñtïmé RÉPL šéttïñg (prévïéw.ròwš, prévïéw.lïñéš, prévïéw.élàpšédThréšhòldMš).···!!] + + + [!!Ùñkñòwñ šéttïñg kéÿ '{0}'.···!!] + + + [!!Àçtïvé thémé: {0}···!!] + + + [!! Wïth ñò àrgùméñt, prïñtš thé àçtïvé thémé. + Wïth à ñàmé, çhàñgéš thé àçtïvé thémé. Màtçhéd bÿ DïšplàÿÑàmé çàšé-ïñšéñšïtïvélÿ, + wïth ThéméÏd àš à fàllbàçk.···!!] + + + [!!Prïñtš òr šwïtçhéš thé àçtïvé thémé.···!!] + + + [!!Šwïtçhéd tò thémé: {0}···!!] + + + [!! Lïštš vàrïàbléš fròm thé šhàréd ÏVàrïàbléŠtòré. Çòlùmñš: Ñàmé, Tÿpé, Prévïéw.···!!] + + + [!!Ñò vàrïàbléš.···!!] + + + [!!Lïštš vàrïàbléš fròm ÏVàrïàbléŠtòré.···!!] + + + [!! Prïñtš thé òùtpùtš òf thé làšt çéll (òr çéll ñ ïf šùpplïéd) ïñ fùll, bÿpàššïñg + thé ròw/lïñé çàpš àpplïéd dùrïñg ñòrmàl réñdérïñg.···!!] + + + [!!Ïñvàlïd òr òùt-òf-ràñgé çéll ïñdéx '{0}'.···!!] + + + [!!Çéll [{0}] hàš ñò òùtpùtš.···!!] + + + [!!Ñò çéllš tò vïéw.···!!] + + + [!!Prïñtš thé làšt çéll'š òùtpùt wïthòùt trùñçàtïòñ.···!!] + + + [!!Ïñštàll Pÿthòñ pàçkàgéš à çéll ïmpòrtš bùt thé éñvïròñméñt dòéš ñòt hàvé. Òñlÿ pàçkàgéš whòšé dïštrïbùtïòñ ñàmé ïš kñòwñ àré ïñštàlléd; àñÿthïñg élšé ïš répòrtéd.···!!] + + + [!!Dïréçtòrÿ tò šçàñ fòr àddïtïòñàl éxtéñšïòñ àššémblïéš.···!!] + + + [!!Làñgùàgé fòr méššàgéš àñd hélp, òñé òf: {0}. Défàùltš tò thé šÿštém làñgùàgé, fàllïñg bàçk tò Éñglïšh.···!!] + + + [!!Pàth tò thé Pÿthòñ ïñtérprétér ùšéd bÿ Pÿthòñ çéllš. Òvérrïdéš àùtòmàtïç dïšçòvérÿ.···!!] + + + [!!défàùlt: {0}···!!] + + + [!!Ñòtébòòk pàràmétérš:···!!] + + + [!!Ïñvàlïd pàràmétér vàlùéš:···!!] + + + [!!Mïššïñg réqùïréd ñòtébòòk pàràmétérš:···!!] + + + [!!réqùïréd···!!] + + + [!!Šùpplÿ vàlùéš wïth --pàràm, òr ùšé --ïñtéràçtïvé tò bé pròmptéd.···!!] + + + [!!Ùñkñòwñ pàràmétér '{0}' ïš ñòt défïñéd ïñ thé ñòtébòòk métàdàtà. Ïñjéçtïñg ït àš à štrïñg.···!!] + + + [!!Pàràmétér défïñïtïòñš àré òñlÿ šùppòrtéd fòr .véršò fïléš. Ïñjéçtïñg àll --pàràm vàlùéš àš ùñtÿpéd štrïñgš.···!!] + + + [!!Vàlùé ïš réqùïréd.···!!] + + + [!!Érròr: {0}···!!] + + + [!!Fàtàl érròr: {0}···!!] + + + [!!Wàrñïñg: {0}···!!] + + + [!!(réçàlléd: çòpÿ àñd pàšté òr édït bélòw)···!!] + + + [!!<òùtpùt: {0}, {1} çhàrš, bïñàrÿ>···!!] + + + [!!Çéll {0}···!!] + + + [!!… {0} mòré lïñé···!!] + + + [!!… {0} mòré lïñéš···!!] + + + [!!(éxéçùtéd ïñ {0})···!!] + + + [!!Éxéçùtïòñ fàïléd.···!!] + + + [!!<ïmàgé: {0}, {1}, šàvéd tò {2}>···!!] + + + [!!<ïmàgé: {0}, fàïléd tò déçòdé: {1}>···!!] + + + [!!... ({0} mòré lïñé)···!!] + + + [!!... ({0} mòré lïñéš)···!!] + + + [!!… {0} mòré ròw···!!] + + + [!!… {0} mòré ròwš···!!] + + + [!!(ñò pàràmétérš)···!!] + + + [!!Çéllš: {0} tòtàl, {1} šùççéédéd, {2} fàïléd···!!] + + + [!!Šùmmàrÿ···!!] + + + [!!Tïmé: {0}š···!!] + + + [!!<òùtpùt: {0}>···!!] + + + [!![wïdgét òùtpùt, šhòwñ whéñ thé ñòtébòòk ïš òpéñéd]···!!] + + + [!!Pàth tò à .véršò, .ïpÿñb, òr .dïb fïlé. Whéñ òmïttéd, štàrtš wïth àñ émptÿ šçràtçh ñòtébòòk.···!!] + + + [!!Çàñçélléd.···!!] + + + [!!{0} çéll···!!] + + + [!!{0} çéllš···!!] + + + [!!Štàrt àñ ïñtéràçtïvé Véršò RÉPL ïñ thé térmïñàl.···!!] + + + [!!Éxéçùtïòñ érròr: {0}···!!] + + + [!!{0} lòàdéd···!!] + + + [!!éxtéñšïòñš:···!!] + + + [!!Tÿpé {0} fòr çòmmàñdš, {1} tò qùït.···!!] + + + [!!kérñél:···!!] + + + [!!ñòtébòòk:···!!] + + + [!!thémé:···!!] + + + [!!<défàùlt>···!!] + + + [!!<ñòñé>···!!] + + + [!!*šçràtçh*···!!] + + + [!!Érròr ïñ métà-çòmmàñd '.{0}': {1}···!!] + + + [!!Whéñ çòmbïñéd wïth <ñòtébòòk>, éxéçùtéš àll lòàdéd çéllš béfòré hàñdïñg çòñtròl tò thé pròmpt.···!!] + + + [!!Pàth tò thé pròmpt hïštòrÿ fïlé. Ùšé 'ñòñé' tò dïšàblé péršïštéñt hïštòrÿ.···!!] + + + [!!Àçtïvé kérñél fòr thé fïršt çéll. Màtçhéd àgàïñšt ÏLàñgùàgéKérñél.KérñélÏd çàšé-ïñšéñšïtïvélÿ. Çàñ bé çhàñgéd àt rùñtïmé wïth .kérñél.···!!] + + + [!!Défàùlt làÿòùt ïd, pàššéd tò .éxpòrt àš ÀçtïvéLàÿòùtÏd ùñléšš thé çòmmàñd òvérrïdéš ït.···!!] + + + [!!Prïñt àvàïlàblé kérñélš àñd éxït.···!!] + + + [!!Prïñt régïštéréd théméš àñd éxït.···!!] + + + [!!Dïšàblé ÀÑŠÏ štÿlïñg. Òùtpùt ïš plàïñ ÙTF-8 téxt.···!!] + + + [!!Fòrçé thé lïñé-òrïéñtéd fàllbàçk pròmpt, bÿpàššïñg PréttÿPròmpt évéñ whéñ thé térmïñàl wòùld šùppòrt ït.···!!] + + + [!!Whéñ thé lòàdéd ñòtébòòk ïš .ïpÿñb, .šàvé (ñò àrg) wrïtéš bàçk tò .ïpÿñb ïñštéàd òf çòñvértïñg tò .véršò. Çéll òùtpùtš àré préšérvéd.···!!] + + + [!!Àçtïvé thémé fòr òùtpùt réñdérïñg. DïšplàÿÑàmé çàšé-ïñšéñšïtïvé wïth ThéméÏd fàllbàçk.···!!] + + + [!!Prïòr éxéçùtïòñ fàïléd.···!!] + + + [!!Véršò RÉPL {0}···!!] + + + [!!Tÿpé {0} fòr thé lïšt.···!!] + + + [!!Ùñkñòwñ métà-çòmmàñd '.{0}'.···!!] + + + [!!Šéššïòñ hàš ùñšàvéd çéllš.···!!] + + + [!!Rùñ {0} fïršt, òr {1} àgàïñ tò dïšçàrd.···!!] + + + [!!Ùšàgé: {0}···!!] + + + [!!Véršò ÇLÏ: éxéçùté, šérvé, àñd çòñvért Véršò ñòtébòòkš.···!!] + + + [!!Ùñhàñdléd érròr: {0}···!!] + + + [!!Pàth tò à .véršò, .ïpÿñb, òr .dïb fïlé.···!!] + + + [!![{0}/{1}] Çéll {0} çòmplétéd ïñ {2}š ({3})···!!] + + + [!!Éxéçùté à ñòtébòòk héàdléššlÿ àñd štréàm çéll òùtpùtš.···!!] + + + [!![{0}/{1}] Éxéçùtïñg çéll {0} ({2})...···!!] + + + [!!Ïñvàlïd çéll šéléçtòr '{0}'. Ùšé à 0-bàšéd ïñdéx òr à çéll GÙÏD.···!!] + + + [!!Ïñvàlïd --pàràm fòrmàt '{0}'. Éxpéçtéd ñàmé=vàlùé.···!!] + + + [!!Lòàdïñg thïrd-pàrtÿ éxtéñšïòñš fròm '{0}'. Théšé éxtéñšïòñš àré àùtò-àppròvéd fòr héàdléšš éxéçùtïòñ.···!!] + + + [!!Éxéçùté òñlÿ thé špéçïfïéd çéll (ïñdéx òr GÙÏD). Màÿ bé répéàtéd.···!!] + + + [!!Štòp éxéçùtïòñ òñ thé fïršt çéll fàïlùré.···!!] + + + [!!Tréàt àñÿthïñg à çéll wrïtéš tò štàñdàrd érròr àš à fàïlùré. Òff bÿ défàùlt, béçàùšé prògréšš bàrš, lòggïñg, àñd wàrñïñgš àré ñòrmàllÿ wrïttéñ théré bÿ prògràmš thàt àré šùççéédïñg. Ùšé thïš tò màké à pïpélïñé štrïçt àbòùt thém.···!!] + + + [!!Ïgñòré pér-çéll véršò:ùï.òùtpùtVïšïbïlïtÿ àñd véršò:ùï.ïñpùtÇòllàpšéd métàdàtà; šhòw àll òùtpùtš ïñ fùll.···!!] + + + [!!Ïñçlùdé màrkdòwñ àñd HTML çéll çòñtéñt ïñ térmïñàl òùtpùt.···!!] + + + [!!Pròmpt fòr mïššïñg réqùïréd pàràmétérš òñ štdïñ ïñštéàd òf fàïlïñg.···!!] + + + [!!Òvérrïdé thé ñòtébòòk'š défàùlt kérñél.···!!] + + + [!!Òùtpùt fòrmàt: téxt, jšòñ, òr ñòñé.···!!] + + + [!!Wrïté òùtpùt tò à fïlé ïñštéàd òf štdòùt. Ïmplïéš --òùtpùt jšòñ ïf ñò fòrmàt špéçïfïéd.···!!] + + + [!!Šét à ñòtébòòk pàràmétér (fòrmàt: ñàmé=vàlùé). Màÿ bé répéàtéd.···!!] + + + [!!Šàvé ùpdàtéd òùtpùtš bàçk tò thé ñòtébòòk fïlé àftér éxéçùtïòñ.···!!] + + + [!!Šhòw réšòlvéd pàràmétér vàlùéš ïñ térmïñàl òùtpùt.···!!] + + + [!!Màxïmùm tòtàl éxéçùtïòñ tïmé ïñ šéçòñdš.···!!] + + + [!!Àllòw lòàdïñg àššémblïéš géñéràtéd dùrïñg thé çùrréñt šéššïòñ wïthòùt çòñšéñt.···!!] + + + [!!Prïñt çéll éxéçùtïòñ prògréšš tò štdérr.···!!] + + + [!!Réfùšïñg šéššïòñ-géñéràtéd éxtéñšïòñ '{0}'. Ùšé --trùšt-lòçàl-àššémblïéš tò àllòw.···!!] + + + [!!Ùñšùppòrtéd òr ïñvàlïd ñòtébòòk fòrmàt '{0}'.···!!] + + + [!!Òptïòñàl ñòtébòòk tò òpéñ òñ štàrtùp.···!!] + + + [!!Làùñçh thé Véršò Blàzòr àpplïçàtïòñ àš à lòçàl wéb šérvér.···!!] + + + [!!Dò ñòt òpéñ à bròwšér tàb òñ štàrtùp.···!!] + + + [!!Dïšàblé HTTPŠ (HTTP òñlÿ).···!!] + + + [!!HTTP pòrt tò lïštéñ òñ.···!!] + + + [!!Whéñ à lòàdéd .ïpÿñb ñòtébòòk ïš šàvéd, wrïté bàçk tò .ïpÿñb ïñštéàd òf çòñvértïñg tò .véršò. Çéll òùtpùtš àré préšérvéd.···!!] + + + [!!Prïñt štàrtùp détàïlš tò štdérr.···!!] + + + [!!Préšš Çtrl+Ç tò štòp.···!!] + + + [!!Véršò ïš rùññïñg àt {0}···!!] + + + [!!Fàïléd tò štàrt šérvér: {0}···!!] + + + [!!Éxtéñšïòñš: {0}···!!] + + + [!!Ñòtébòòk: {0}···!!] + + + [!!Çòmmàñd···!!] + + + [!!Déšçrïptïòñ···!!] + + + [!!Dïšplàÿ ñàmé···!!] + + + [!!Éxtéñšïòñš···!!] + + + [!!Fòrmàt···!!] + + + [!!Ïd···!!] + + + [!!Kïñd···!!] + + + [!!Làñgùàgé···!!] + + + [!!Ñàmé···!!] + + + [!!Prévïéw···!!] + + + [!!Prïòrïtÿ···!!] + + + [!!Štàtùš···!!] + + + [!!Thémé···!!] + + + [!!Tÿpé···!!] + + + [!!Véršïòñ···!!] + + \ No newline at end of file diff --git a/src/Verso.Cli/Resources/Strings.resx b/src/Verso.Cli/Resources/Strings.resx new file mode 100644 index 00000000..0750cccf --- /dev/null +++ b/src/Verso.Cli/Resources/Strings.resx @@ -0,0 +1,1103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a2c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a2c561934e089 + + + Path to the source notebook file. + Help for the file argument of convert and export. + + + Convert between notebook formats. + Summary of the convert command in 'verso --help'. + + + Converted '{0}' to '{1}' + Printed on success. {0} and {1} are file paths. + + + Unsupported format '{0}'. Supported: {1} + Reported while reading the command line. {0} is what the reader typed, {1} the list of format names that do work. + + + Converting to '{0}' format is not yet supported. + {0} is a format name. + + + Output file path. Defaults to input filename with the new extension. + Help for --output. + + + Remove all cell outputs from the converted notebook. + Help for --strip-outputs. + + + Target format: verso, ipynb, md, or dib. + Help for --to. The four values are typed at a keyboard and stay as written. + + + Post-processing failed: {0} + {0} is the reason. + + + Serialization failed: {0} + {0} is the reason. + + + Available kernels: {0}. + Follows the message above. {0} is a list of language names such as csharp, python, which are identifiers and are not translated. + + + Available themes: {0}. + Follows the message above. {0} is a list of theme names. + + + Failed to deserialize '{0}': {1} + {0} is a file path, {1} the reason the reader gave. + + + Export format '{0}' is not registered. + {0} is the format name the reader asked for. + + + Input file not found: {0} + {0} is a file path. + + + Kernel '{0}' is not registered. + {0} is the kernel the reader asked for. + + + Multiple export actions share display name '{0}'. Disambiguate by ActionId: {1}. + Two installed extensions offer an export format under the same name. ActionId is an identifier and is not translated. {1} is a list of them. + + + Multiple themes share display name '{0}'. Disambiguate by ThemeId: {1}. + Two installed themes carry the same name. ThemeId is an identifier and is not translated. {1} is a list of them. + + + Notebook file not found: {0} + {0} is a file path. + + + Theme '{0}' is not registered. + {0} is the theme name the reader asked for. + + + Unsupported notebook format '{0}'. Supported formats: {1} + {0} is a file extension such as .txt, {1} the list of extensions that do work. Extensions are the same in every language. + + + Unsupported output format '{0}'. Supported formats: {1} + {0} is a format name such as ipynb, {1} the list of names that do work. Format names are the same in every language. + + + Export action '{0}' threw: {1} + {0} is the action's identifier, {1} the reason. + + + Export a notebook via an ExportMenu toolbar action. + Summary of the export command in 'verso --help'. ExportMenu is an identifier and stays as written. + + + Exported '{0}' to '{1}' + Printed on success. {0} and {1} are file paths. + + + Notebook execution reported errors. Aborting export. + Printed when --execute was given and a cell failed. + + + --format is required. + Followed by a line saying where to find the formats that are installed. + + + <input> is required unless --list is specified. + <input> names the file argument as the help text spells it, and stays as written. + + + No export actions are registered. + Printed by --list when nothing installed offers an export format. + + + Export action '{0}' did not produce a file. + {0} is the action's identifier. + + + Execute the notebook before exporting so stored outputs are refreshed. + Help for --execute. + + + Export format, matched against the DisplayName of a registered IToolbarAction whose placement is ExportMenu. Case-insensitive. Quote values containing whitespace. Use --list to see installed formats. + Help for --format. DisplayName, IToolbarAction, and ExportMenu are identifiers and stay as written. + + + Layout id to apply during export, exposed as ActiveLayoutId on the action context. + Help for --layout. ActiveLayoutId is an identifier and stays as written. + + + List registered export actions (DisplayName, ActionId, Description) and exit. + Help for --list. The three names in brackets are identifiers and stay as written. + + + List registered themes (DisplayName, Kind, Description) and exit. + Help for --list-themes. The three names in brackets are identifiers and stay as written. + + + Output file path. If omitted, the exporter's suggested filename is written to the current directory. + Help for --output. + + + DisplayName of a registered theme, matched case-insensitively. Quote values with whitespace. ThemeId is accepted as a fallback to disambiguate display-name collisions. Use --list-themes to see installed themes. + Help for --theme. DisplayName and ThemeId are identifiers and stay as written. + + + Run '{0}' for details. + Follows an error. {0} is a command typed at a keyboard and is the same in every language. + + + Run '{0}' to see available formats. + Follows an error. {0} is a command typed at a keyboard and is the same in every language. + + + Display Verso CLI version, runtime, and extension information. + Summary of the info command in 'verso --help'. + + + Extensions: + Heading above the installed extensions in the info listing. + + + Formatters: + Heading above the installed output formatters in the info listing. + + + Serializers: + Heading above the notebook formats that can be read and written, in the info listing. + + + Engine: + Label in the info listing, against the Verso version. The labels are padded to line up, so keep them close in length to each other. + + + Runtime: + Label in the info listing, against the .NET version. The labels are padded to line up, so keep them close in length to each other. + + + No kernels are registered. + Printed by --list-kernels when nothing installed offers a kernel. + + + No themes are registered. + Printed by --list-themes when nothing installed offers a theme. + + + Clears the terminal screen. Session state (kernel variables, notebook cells) + is preserved; only the scrollback is cleared. + Body of '.help clear'. The two leading spaces on each line are indentation and should be kept. + + + Clears the terminal. + One line about .clear, in the table .help prints. + + + Serializes the current session notebook to <path> using the serializer whose + FileExtensions include the target extension. Does not change the session's + loaded path. Identical resolution to 'verso convert'. + Body of '.help convert'. <path>, FileExtensions, and 'verso convert' stay as written. The two leading spaces on each line are indentation and should be kept. + + + Converted the session notebook to {0} ({1}) + {0} is a file path, {1} a count of cells already written out as a phrase. + + + Convert failed: {0} + {0} is the reason. + + + Writes the session notebook to <path> using the serializer matching its extension. + One line about .convert, in the table .help prints. <path> names the argument as the help text spells it. + + + Exits the REPL. When unsaved cells exist, prompts for confirmation + unless confirmOnExit is disabled in user settings. + Body of '.help exit'. confirmOnExit is a setting name and stays as written. The two leading spaces on each line are indentation and should be kept. + + + Exits the REPL. + One line about .exit, in the table .help prints. + + + Dispatches to an IToolbarAction registered with ToolbarPlacement.ExportMenu. + Format is matched by DisplayName (case-insensitive), ActionId as fallback. + Theme is matched by DisplayName (case-insensitive), ThemeId as fallback. + Without --output, writes the action's suggested filename to the current directory. + Identical to 'verso export'. + Body of '.help export'. Every name in this block is an identifier or a command and stays as written. The two leading spaces on each line are indentation and should be kept. + + + Exported the session notebook to {0} ({1}) + {0} is a file path, {1} a count of cells already written out as a phrase. + + + Missing value for {0}. + {0} is an option name such as --format, typed at a keyboard, and is the same in every language. + + + Exports the session notebook via an ExportMenu toolbar action. + One line about .export, in the table .help prints. ExportMenu is an identifier and stays as written. + + + Unknown argument: {0} + {0} is what the reader typed. + + + With no argument, prints an overview of all meta-commands. + With a name, prints detailed help for that command. + Body of '.help help'. The two leading spaces on each line are indentation and should be kept. + + + Prints meta-command help. + One line about .help, in the table .help prints. + + + Prints the last n submitted cells (default 20). Each entry shows the input counter + and a preview of the first non-empty line of source. + Body of '.help history'. The two leading spaces on each line are indentation and should be kept. + + + No history. + Printed by .history when nothing has been submitted yet. + + + Invalid count '{0}'. + {0} is what the reader typed after .history. + + + Prints recent cell submissions. + One line about .history, in the table .help prints. + + + Active kernel: {0} + {0} is a language name such as csharp, which is an identifier and stays as written. + + + With no argument, prints the active kernel. + With an id (LanguageId, matched case-insensitively), switches the active kernel + for subsequent cells. Variables already declared in the prior kernel remain in + that kernel's scope. + Body of '.help kernel'. LanguageId is an identifier and stays as written. The two leading spaces on each line are indentation and should be kept. + + + Prints or switches the active kernel. + One line about .kernel, in the table .help prints. + + + Switched to kernel: {0} ({1}) + {0} is a language name such as csharp, {1} the name the kernel shows itself under. + + + Kernel warm-up failed: {0} + The kernel was started early so the first keystroke would not wait for it, and that failed. {0} is the reason. + + + Active layout: {0} + {0} is a layout's identifier. + + + Layout cleared. + Printed when .layout none was typed. + + + With no argument, prints the active layout id. + With an id, sets the default ActiveLayoutId for subsequent .export calls. + Pass 'none' to clear the layout. + Body of '.help layout'. ActiveLayoutId is an identifier, and .export and 'none' are typed at a keyboard; all stay as written. The two leading spaces on each line are indentation and should be kept. + + + Layout set to: {0} + {0} is a layout's identifier. + + + Prints or sets the default export layout. + One line about .layout, in the table .help prints. + + + Where <kind> is one of: + kernels, themes, formatters, renderers, serializers, extensions, exporters + Prints a table of the registered items for that capability. + Body of '.help list'. The seven words on the second line are typed at a keyboard and stay as written. The leading spaces are indentation and should be kept. + + + No items registered. + Printed by .list when nothing installed offers the kind that was asked for. + + + Lists registered extension capabilities. + One line about .list, in the table .help prints. + + + Unknown list kind '{0}'. + {0} is what the reader typed after .list. + + + Valid kinds: {0}. + Follows the message above. {0} is the list of kinds, which are typed at a keyboard and are the same in every language. + + + Deserializes the file at <path> through the matching serializer and installs it + as the session notebook. Prompts to save unsaved changes first. Kernel state + (variables) is preserved: run .reset first for a clean start. + Body of '.help load'. <path> and .reset stay as written. The two leading spaces on each line are indentation and should be kept. + + + Loaded {0} from {1} + {0} is a count of cells, already written out as a phrase. {1} is a file path. + + + Failed to load: {0} + {0} is the reason. + + + File not found: {0} + {0} is a file path. + + + Loads a notebook from disk, replacing the session notebook. + One line about .load, in the table .help prints. + + + One-shot: the next submission is appended as a markdown cell instead of a code cell. + After the cell is appended, the REPL reverts to code mode. + Body of '.help md'. The two leading spaces on each line are indentation and should be kept. + + + The next cell will be markdown. + Printed once .md has been typed, so it is clear the change is waiting. + + + Marks the next submission as a markdown cell. + One line about .md, in the table .help prints. + + + Loads cell n's source into the prompt buffer as if the user had typed it. + Pressing Enter submits it as a new cell; the original cell remains untouched. + Out-of-range indices produce an error without clearing the buffer. + Body of '.help recall'. Enter is a key name and stays as written. The two leading spaces on each line are indentation and should be kept. + + + History contains {0}. + Follows the message above. {0} is a count of cells, already written out as a phrase. + + + Cell [{0}] is out of range. + {0} is the number the reader asked for. + + + Recalled cell [{0}]. Edit it, then press Enter and a blank line (or ;;) to submit. + {0} is the cell's number. Enter is a key name and ;; is typed at a keyboard; both stay as written. + + + Loads a prior cell's source into the prompt for editing. + One line about .recall, in the table .help prints. + + + Usage: {0}, where <n> is a cell index counted from one, as shown by {1}. + {0} and {1} are commands typed at a keyboard and are the same in every language. <n> names the argument as the help text spells it. + + + Re-executes cell n (or range n..m, or every cell with 'all') verbatim, + appending each as a new cell. Does not mutate prior cells. A range submits + cells individually so each renders its own outputs; failures within a range + do not stop the rest unless --fail-fast. + Body of '.help rerun'. 'all' and --fail-fast are typed at a keyboard and stay as written. The two leading spaces on each line are indentation and should be kept. + + + Execution error in rerun [{0}]: {1} + {0} is the cell's number, {1} the reason. + + + Invalid cell index '{0}'. + {0} is what the reader typed. + + + Invalid range '{0}'. Expected <n>..<m> with m >= n. + {0} is what the reader typed. <n>..<m> describes the shape it should have and stays as written. + + + No cells to rerun. + Printed by .rerun all when the session has no cells. + + + Range [{0}..{1}] exceeds the history length ({2}). + {0} and {1} are the ends of the range asked for, {2} how many cells there are. + + + Re-executes a prior cell (or a range) as new cells. + One line about .rerun, in the table .help prints. + + + Rebuilds the kernel session, clearing all variables and runtime state. + The notebook's cell history (cells already typed) is preserved, so .save + still captures them. Variables declared before .reset are gone. + Body of '.help reset'. .save and .reset are typed at a keyboard and stay as written. The two leading spaces on each line are indentation and should be kept. + + + Kernel state reset. + Printed once .reset has finished. + + + Resets kernel state; keeps cell history. + One line about .reset, in the table .help prints. + + + Converting to .verso; use --preserve-format to keep {0}. + {0} is a file extension such as .ipynb. .verso and --preserve-format stay as written. + + + Serializes the session notebook. When <path> is omitted, saves to the original + loaded path (if any) or reports an error. Format is inferred from the extension. + Without --preserve-format, a .save with no arg against an .ipynb-loaded notebook + converts to a sibling .verso file; with --preserve-format the original format is kept. + Body of '.help save'. <path>, --preserve-format, .save, .ipynb, and .verso all stay as written. The two leading spaces on each line are indentation and should be kept. + + + Saved {0} to {1} + {0} is a count of cells, already written out as a phrase. {1} is a file path. + + + Failed to save: {0} + {0} is the reason. + + + A path is required when the session has no loaded notebook. + Printed when .save was typed with nothing after it and the session was started without a file. + + + Writes the session notebook to disk. + One line about .save, in the table .help prints. + + + Updates one of the runtime REPL settings. + Known keys: preview.rows, preview.lines, preview.elapsedThresholdMs, confirmOnExit. + Body of '.help set'. The four names on the second line are setting names and stay as written. The two leading spaces on each line are indentation and should be kept. + + + Try {0} for the list of keys. + Follows a usage line. {0} is a command typed at a keyboard and is the same in every language. + + + Invalid true or false value for {0}: '{1}' + {0} is a setting name and stays as written, {1} is what the reader typed. 'true' and 'false' are typed at a keyboard and stay as written. + + + Invalid whole number for {0}: '{1}' + {0} is a setting name and stays as written, {1} is what the reader typed. + + + Invalid non-negative whole number for {0}: '{1}' + {0} is a setting name and stays as written, {1} is what the reader typed. + + + Known keys: {0}. + Follows the message above. {0} is the list of setting names, which stay as written. + + + Sets a runtime REPL setting (preview.rows, preview.lines, preview.elapsedThresholdMs). + One line about .set, in the table .help prints. The three names in brackets are setting names and stay as written. + + + Unknown setting key '{0}'. + {0} is what the reader typed. + + + Active theme: {0} + {0} is a theme's name. + + + With no argument, prints the active theme. + With a name, changes the active theme. Matched by DisplayName case-insensitively, + with ThemeId as a fallback. + Body of '.help theme'. DisplayName and ThemeId are identifiers and stay as written. The two leading spaces on each line are indentation and should be kept. + + + Prints or switches the active theme. + One line about .theme, in the table .help prints. + + + Switched to theme: {0} + {0} is a theme's name. + + + Lists variables from the shared IVariableStore. Columns: Name, Type, Preview. + Body of '.help vars'. IVariableStore is an identifier and stays as written; the three column names are translated elsewhere and should match those translations. The two leading spaces are indentation and should be kept. + + + No variables. + Printed by .vars when nothing has been declared yet. + + + Lists variables from IVariableStore. + One line about .vars, in the table .help prints. IVariableStore is an identifier and stays as written. + + + Prints the outputs of the last cell (or cell n if supplied) in full, bypassing + the row/line caps applied during normal rendering. + Body of '.help view'. The two leading spaces on each line are indentation and should be kept. + + + Invalid or out-of-range cell index '{0}'. + {0} is what the reader typed. + + + Cell [{0}] has no outputs. + {0} is the cell's number. + + + No cells to view. + Printed by .view when the session has no cells. + + + Prints the last cell's output without truncation. + One line about .view, in the table .help prints. + + + Install Python packages a cell imports but the environment does not have. Only packages whose distribution name is known are installed; anything else is reported. + Help for --auto-install. + + + Directory to scan for additional extension assemblies. + Help for --extensions, taken by run, serve, convert, export, and repl. + + + Language for messages and help, one of: {0}. Defaults to the system language, falling back to English. + Help for --language. {0} is the list of language tags, which are identifiers rather than words and are the same in every language. + + + Path to the Python interpreter used by Python cells. Overrides automatic discovery. + Help for --python. + + + default: {0} + Written after a parameter's type to say what it falls back to. {0} is the value. + + + Notebook parameters: + Heading above the questions --interactive asks. + + + Invalid parameter values: + Heading, followed by one indented line per parameter. + + + Missing required notebook parameters: + Heading, followed by one indented line per parameter. + + + required + Written after a parameter's type to say a value must be given, as in 'int, required'. + + + Supply values with --param, or use --interactive to be prompted. + Follows the list of missing parameters. The two option names stay as written. + + + Unknown parameter '{0}' is not defined in the notebook metadata. Injecting it as a string. + {0} is the name the reader passed to --param. + + + Parameter definitions are only supported for .verso files. Injecting all --param values as untyped strings. + .verso and --param stay as written. + + + Value is required. + Printed when --interactive was answered with nothing and the parameter has no fallback. + + + Error: {0} + Written to standard error when something the reader asked for could not be done. {0} is the rest of the line. + + + Fatal error: {0} + Written when the REPL cannot start or cannot carry on. {0} is the reason. + + + Warning: {0} + Written to standard error when the run carries on but something is worth knowing. {0} is the rest of the line. + + + (recalled: copy and paste or edit below) + Printed above a cell the reader asked to bring back, because the prompt cannot be filled in for them. + + + <output: {0}, {1} chars, binary> + Stands in for an output that is not text. {0} is a MIME type, {1} how long it is. + + + Cell {0} + Heading above one cell's output. {0} is the cell's position in the notebook, counted from zero. Kept short: it sits on a line of rules that is padded to a fixed width. + + + … {0} more line + Closes an output the REPL shortened. Used when {0} is 1, paired with the entry below. + + + … {0} more lines + Closes an output the REPL shortened. Used for every count other than 1. A language with one form for both translates this the same as the entry above. + + + (executed in {0}) + Printed under a cell that took long enough to be worth reporting. {0} is a duration with its unit already attached, such as 1.20 s. + + + Execution failed. + Stands in for the reason when a cell failed without saying why. + + + <image: {0}, {1}, saved to {2}> + Stands in for a picture, which a terminal cannot draw. {0} is a MIME type, {1} a file size, {2} where it was written so it can be opened. + + + <image: {0}, failed to decode: {1}> + Stands in for a picture that could not be read. {0} is a MIME type, {1} the reason. + + + ... ({0} more line) + Closes a shortened output. Used when {0} is 1, paired with the entry below. + + + ... ({0} more lines) + Closes a shortened output. Used for every count other than 1. A language with one form for both translates this the same as the entry above. + + + … {0} more row + Closes a shortened table. Used when {0} is 1, paired with the entry below. + + + … {0} more rows + Closes a shortened table. Used for every count other than 1. A language with one form for both translates this the same as the entry above. + + + (no parameters) + Stands in for the values under a parameters cell when the notebook declares none. + + + Cells: {0} total, {1} succeeded, {2} failed + First line of the summary. + + + Summary + Heading above the last block a run prints. Kept short: it sits on a line of rules. + + + Time: {0}s + Second line of the summary. {0} is a number of seconds; s is the symbol for seconds. + + + <output: {0}> + Heading above an output of a kind the terminal has no special handling for. {0} is a MIME type and stays as written. + + + [widget output, shown when the notebook is opened] + Stands in for an output a terminal cannot draw, because it only exists once a browser has run it. + + + Path to a .verso, .ipynb, or .dib file. When omitted, starts with an empty scratch notebook. + Help for the file argument of repl. + + + Cancelled. + Printed when Ctrl+C stopped a cell. + + + {0} cell + Counts the cells in a notebook, dropped into another message. Used when {0} is 1, paired with the entry below. + + + {0} cells + Counts the cells in a notebook, dropped into another message. Used for every count other than 1. A language with one form for both translates this the same as the entry above. + + + Start an interactive Verso REPL in the terminal. + Summary of the repl command in 'verso --help'. REPL is the usual name for a prompt that reads, runs, and prints; keep the English acronym where that is what practitioners say. + + + Execution error: {0} + Printed when running a cell failed before the kernel could report it. {0} is the reason. + + + {0} loaded + Value against the extensions label in the starting banner. {0} is how many were loaded. + + + extensions: + Label in the banner printed when the REPL starts. The four labels line up, so keep them close in length to each other. + + + Type {0} for commands, {1} to quit. + Last line of the starting banner. {0} and {1} are .help and .exit, typed at a keyboard, and are the same in every language. + + + kernel: + Label in the banner printed when the REPL starts. The four labels line up, so keep them close in length to each other. + + + notebook: + Label in the banner printed when the REPL starts. The four labels line up, so keep them close in length to each other. + + + theme: + Label in the banner printed when the REPL starts. The four labels line up, so keep them close in length to each other. + + + <default> + Stands in for the theme when none was chosen. Written in angle brackets so it cannot be mistaken for the name of one. + + + <none> + Stands in for the kernel or the layout when none is chosen. Written in angle brackets so it cannot be mistaken for the name of one. + + + *scratch* + Stands in for the file name when the session was started without one. Written in asterisks so it cannot be mistaken for a path. + + + Error in meta-command '.{0}': {1} + {0} is the command's name, {1} the reason it failed. + + + When combined with <notebook>, executes all loaded cells before handing control to the prompt. + Help for --execute. <notebook> names the file argument as the help text spells it. + + + Path to the prompt history file. Use 'none' to disable persistent history. + Help for --history. 'none' is typed at a keyboard and stays as written. + + + Active kernel for the first cell. Matched against ILanguageKernel.KernelId case-insensitively. Can be changed at runtime with .kernel. + Help for --kernel. ILanguageKernel.KernelId is an identifier and .kernel is typed at a keyboard; both stay as written. + + + Default layout id, passed to .export as ActiveLayoutId unless the command overrides it. + Help for --layout. .export is typed at a keyboard and ActiveLayoutId is an identifier; both stay as written. + + + Print available kernels and exit. + Help for --list-kernels. + + + Print registered themes and exit. + Help for --list-themes. + + + Disable ANSI styling. Output is plain UTF-8 text. + Help for --no-color. ANSI and UTF-8 are standard names and stay as written. + + + Force the line-oriented fallback prompt, bypassing PrettyPrompt even when the terminal would support it. + Help for --plain. PrettyPrompt is a library name and stays as written. + + + When the loaded notebook is .ipynb, .save (no arg) writes back to .ipynb instead of converting to .verso. Cell outputs are preserved. + Help for --preserve-format. .save is typed at a keyboard and stays as written. + + + Active theme for output rendering. DisplayName case-insensitive with ThemeId fallback. + Help for --theme. DisplayName and ThemeId are identifiers and stay as written. + + + Prior execution failed. + Stands in for the reason when a notebook records that a cell failed but not why. + + + Verso REPL {0} + Title given to a session started without a file, and saved into the notebook. {0} is the date and time it started. + + + Type {0} for the list. + Follows the message above. {0} is .help, typed at a keyboard, and is the same in every language. + + + Unknown meta-command '.{0}'. + A meta-command is one of the dot-prefixed commands the REPL answers itself. {0} is what was typed after the dot. + + + Session has unsaved cells. + Printed when something would discard work. Followed by the line below. + + + Run {0} first, or {1} again to discard. + Follows the message above. {0} and {1} are commands typed at a keyboard and are the same in every language. + + + Usage: {0} + Printed when a REPL command was typed wrongly. {0} is the shape it should have taken, which is typed at a keyboard and is the same in every language. + + + Verso CLI: execute, serve, and convert Verso notebooks. + First line of 'verso --help'. Verso is the product's name and is the same in every language. + + + Unhandled error: {0} + Last resort, printed when a command failed in a way nothing else caught. {0} is the reason. + + + Path to a .verso, .ipynb, or .dib file. + Help for the file argument of run. The extensions are the same in every language. + + + [{0}/{1}] Cell {0} completed in {2}s ({3}) + Progress line, only with --verbose. {2} is a number of seconds, {3} the recorded status, which is an identifier such as Success. + + + Execute a notebook headlessly and stream cell outputs. + Summary of the run command in 'verso --help'. + + + [{0}/{1}] Executing cell {0} ({2})... + Progress line, only with --verbose. {0} is the cell's position, {1} how many there are, {2} its language, which is an identifier such as csharp. + + + Invalid cell selector '{0}'. Use a 0-based index or a cell GUID. + {0} is what the reader passed to --cell. + + + Invalid --param format '{0}'. Expected name=value. + {0} is what the reader typed. 'name=value' describes the shape it should have and stays as written. + + + Loading third-party extensions from '{0}'. These extensions are auto-approved for headless execution. + {0} is a directory path. + + + Execute only the specified cell (index or GUID). May be repeated. + Help for --cell. + + + Stop execution on the first cell failure. + Help for --fail-fast. + + + Treat anything a cell writes to standard error as a failure. Off by default, because progress bars, logging, and warnings are normally written there by programs that are succeeding. Use this to make a pipeline strict about them. + Help for --fail-on-stderr. + + + Ignore per-cell verso:ui.outputVisibility and verso:ui.inputCollapsed metadata; show all outputs in full. + Help for --ignore-view-state. The two metadata keys are identifiers and stay as written. + + + Include markdown and HTML cell content in terminal output. + Help for --include-markdown. + + + Prompt for missing required parameters on stdin instead of failing. + Help for --interactive. + + + Override the notebook's default kernel. + Help for --kernel. + + + Output format: text, json, or none. + Help for --output. The three values are typed at a keyboard and stay as written. + + + Write output to a file instead of stdout. Implies --output json if no format specified. + Help for --output-file. + + + Set a notebook parameter (format: name=value). May be repeated. + Help for --param. 'name=value' describes what is typed and stays as written. + + + Save updated outputs back to the notebook file after execution. + Help for --save. + + + Show resolved parameter values in terminal output. + Help for --show-parameters. + + + Maximum total execution time in seconds. + Help for --timeout. + + + Allow loading assemblies generated during the current session without consent. + Help for --trust-local-assemblies. + + + Print cell execution progress to stderr. + Help for --verbose. + + + Refusing session-generated extension '{0}'. Use --trust-local-assemblies to allow. + {0} is a package name. + + + Unsupported or invalid notebook format '{0}'. + {0} is a file extension. + + + Optional notebook to open on startup. + Help for the file argument of serve. + + + Launch the Verso Blazor application as a local web server. + Summary of the serve command in 'verso --help'. + + + Do not open a browser tab on startup. + Help for --no-browser. + + + Disable HTTPS (HTTP only). + Help for --no-https. + + + HTTP port to listen on. + Help for --port. + + + When a loaded .ipynb notebook is saved, write back to .ipynb instead of converting to .verso. Cell outputs are preserved. + Help for --preserve-format. + + + Print startup details to stderr. + Help for --verbose. + + + Press Ctrl+C to stop. + Follows the line above. Ctrl+C is a key combination and is the same in every language. + + + Verso is running at {0} + Printed once the server is listening. {0} is a web address. + + + Failed to start server: {0} + {0} is the reason. + + + Extensions: {0} + Startup detail, only with --verbose. {0} is a directory path. + + + Notebook: {0} + Startup detail, only with --verbose. {0} is a file path. + + + Command + Column heading over the REPL commands listed by .help. Kept short: columns are padded to their widest entry. + + + Description + Column heading over a line describing the row. Kept short: columns are padded to their widest entry. + + + Display name + Column heading over the name a kernel shows itself under. Kept short: columns are padded to their widest entry. + + + Extensions + Column heading over the file extensions a format is read from, such as .verso. Not the add-ons, which have their own heading. Kept short: columns are padded to their widest entry. + + + Format + Column heading over the name of an export format or a notebook format. Kept short: columns are padded to their widest entry. + + + Id + Column heading over an extension's identifier. Kept short: columns are padded to their widest entry. + + + Kind + Column heading over whether a theme is light or dark. Kept short: columns are padded to their widest entry. + + + Language + Column heading over the language a kernel runs. Kept short: columns are padded to their widest entry. + + + Name + Column heading over a variable's or a formatter's name. Kept short: columns are padded to their widest entry. + + + Preview + Column heading over the first part of a variable's value. Kept short: columns are padded to their widest entry. + + + Priority + Column heading over the order formatters are tried in. Kept short: columns are padded to their widest entry. + + + Status + Column heading over whether an extension loaded. Kept short: columns are padded to their widest entry. + + + Theme + Column heading over a theme's name. Kept short: columns are padded to their widest entry. + + + Type + Column heading over the type of a variable, as in what kind of value it holds. Kept short: columns are padded to their widest entry. + + + Version + Column heading over an extension's version number. Kept short: columns are padded to their widest entry. + + diff --git a/src/Verso.Cli/Resources/Strings.zh-Hans.resx b/src/Verso.Cli/Resources/Strings.zh-Hans.resx new file mode 100644 index 00000000..54378aa6 --- /dev/null +++ b/src/Verso.Cli/Resources/Strings.zh-Hans.resx @@ -0,0 +1,851 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 源笔记本文件的路径。 + + + 在笔记本格式之间进行转换。 + + + 已将“{0}”转换为“{1}” + + + 不支持的格式“{0}”。支持的格式:{1} + + + 尚不支持转换为“{0}”格式。 + + + 输出文件路径。默认为输入文件名加上新的扩展名。 + + + 从转换后的笔记本中移除所有单元格输出。 + + + 目标格式:verso、ipynb、md 或 dib。 + + + 后处理失败:{0} + + + 序列化失败:{0} + + + 可用的内核:{0}。 + + + 可用的主题:{0}。 + + + 反序列化“{0}”失败:{1} + + + 导出格式“{0}”未注册。 + + + 找不到输入文件:{0} + + + 内核“{0}”未注册。 + + + 多个导出操作共用显示名称“{0}”。请使用 ActionId 加以区分:{1}。 + + + 多个主题共用显示名称“{0}”。请使用 ThemeId 加以区分:{1}。 + + + 找不到笔记本文件:{0} + + + 主题“{0}”未注册。 + + + 不支持的笔记本格式“{0}”。支持的格式:{1} + + + 不支持的输出格式“{0}”。支持的格式:{1} + + + 导出操作“{0}”引发了异常:{1} + + + 通过 ExportMenu 工具栏操作导出笔记本。 + + + 已将“{0}”导出到“{1}” + + + 笔记本执行报告了错误。正在中止导出。 + + + 必须指定 --format。 + + + 除非指定 --list,否则必须提供 <input>。 + + + 未注册任何导出操作。 + + + 导出操作“{0}”未生成文件。 + + + 在导出前执行笔记本,以刷新存储的输出。 + + + 导出格式,与放置位置为 ExportMenu 的已注册 IToolbarAction 的 DisplayName 匹配。不区分大小写。含空格的值请加引号。使用 --list 查看已安装的格式。 + + + 导出期间应用的布局 ID,在操作上下文中公开为 ActiveLayoutId。 + + + 列出已注册的导出操作(DisplayName、ActionId、Description)并退出。 + + + 列出已注册的主题(DisplayName、Kind、Description)并退出。 + + + 输出文件路径。如果省略,则将导出程序建议的文件名写入当前目录。 + + + 已注册主题的 DisplayName,不区分大小写匹配。含空格的值请加引号。显示名称冲突时,可用 ThemeId 作为后备加以区分。使用 --list-themes 查看已安装的主题。 + + + 运行“{0}”了解详细信息。 + + + 运行“{0}”查看可用的格式。 + + + 显示 Verso CLI 的版本、运行时和扩展信息。 + + + 扩展: + + + 格式化程序: + + + 序列化程序: + + + 引擎: + + + 运行时: + + + 未注册任何内核。 + + + 未注册任何主题。 + + + 清除终端屏幕。会话状态(内核变量、笔记本单元格)会保留, + 只清除回滚缓冲区。 + + + 清除终端。 + + + 使用 FileExtensions 包含目标扩展名的序列化程序,将当前会话笔记本 + 序列化到 <path>。不会更改会话已加载的路径。解析方式与 + 'verso convert' 完全相同。 + + + 已将会话笔记本转换到 {0}({1}) + + + 转换失败:{0} + + + 使用与扩展名匹配的序列化程序将会话笔记本写入 <path>。 + + + 退出 REPL。存在未保存的单元格时会提示确认, + 除非在用户设置中禁用了 confirmOnExit。 + + + 退出 REPL。 + + + 调度到以 ToolbarPlacement.ExportMenu 注册的 IToolbarAction。 + 格式按 DisplayName 匹配(不区分大小写),ActionId 作为后备。 + 主题按 DisplayName 匹配(不区分大小写),ThemeId 作为后备。 + 未指定 --output 时,将操作建议的文件名写入当前目录。 + 与 'verso export' 完全相同。 + + + 已将会话笔记本导出到 {0}({1}) + + + {0} 缺少值。 + + + 通过 ExportMenu 工具栏操作导出会话笔记本。 + + + 未知参数:{0} + + + 不带参数时,打印所有元命令的概览。 + 带名称时,打印该命令的详细帮助。 + + + 打印元命令帮助。 + + + 打印最近提交的 n 个单元格(默认 20 个)。每一项显示输入计数器 + 以及源代码第一个非空行的预览。 + + + 无历史记录。 + + + 无效的数量“{0}”。 + + + 打印最近提交的单元格。 + + + 活动内核:{0} + + + 不带参数时,打印活动内核。 + 带 ID(LanguageId,不区分大小写匹配)时,为后续单元格切换 + 活动内核。先前内核中已声明的变量仍留在该内核的 + 作用域内。 + + + 打印或切换活动内核。 + + + 已切换到内核:{0}({1}) + + + 内核预热失败:{0} + + + 活动布局:{0} + + + 已清除布局。 + + + 不带参数时,打印活动布局 ID。 + 带 ID 时,为后续的 .export 调用设置默认的 ActiveLayoutId。 + 传入 'none' 可清除布局。 + + + 布局已设置为:{0} + + + 打印或设置默认导出布局。 + + + 其中 <kind> 为下列之一: + kernels, themes, formatters, renderers, serializers, extensions, exporters + 打印该功能已注册项的表格。 + + + 未注册任何项。 + + + 列出已注册的扩展功能。 + + + 未知的列表种类“{0}”。 + + + 有效的种类:{0}。 + + + 通过匹配的序列化程序反序列化 <path> 处的文件,并将其安装为 + 会话笔记本。会先提示保存未保存的更改。内核状态(变量)会保留: + 如需干净的开始,请先运行 .reset。 + + + 已从 {1} 加载 {0} + + + 加载失败:{0} + + + 找不到文件:{0} + + + 从磁盘加载笔记本,替换会话笔记本。 + + + 一次性生效:下一次提交将作为 markdown 单元格而非代码单元格追加。 + 该单元格追加后,REPL 恢复为代码模式。 + + + 下一个单元格将是 markdown。 + + + 将下一次提交标记为 markdown 单元格。 + + + 将第 n 个单元格的源代码加载到提示缓冲区,就像是手动键入的一样。 + 按 Enter 会将它作为新单元格提交;原单元格保持不变。 + 超出范围的索引会报错,但不会清除缓冲区。 + + + 历史记录中有 {0}。 + + + 单元格 [{0}] 超出范围。 + + + 已调回单元格 [{0}]。编辑后,按 Enter 再输入一个空行(或 ;;)即可提交。 + + + 将先前单元格的源代码加载到提示符中以供编辑。 + + + 用法:{0},其中 <n> 是从 1 开始计数的单元格索引,如 {1} 所示。 + + + 原样重新执行第 n 个单元格(或范围 n..m,或用 'all' 执行每个单元格), + 并将每一个作为新单元格追加。不会改动先前的单元格。范围会逐个提交 + 单元格,因此每个单元格都渲染自己的输出;除非指定 --fail-fast,否则 + 范围内的失败不会中断其余单元格。 + + + 重新运行 [{0}] 时出现执行错误:{1} + + + 无效的单元格索引“{0}”。 + + + 无效的范围“{0}”。应为 <n>..<m>,且 m >= n。 + + + 没有可重新运行的单元格。 + + + 范围 [{0}..{1}] 超出了历史记录的长度({2})。 + + + 将先前的单元格(或一个范围)作为新单元格重新执行。 + + + 重建内核会话,清除所有变量和运行时状态。 + 笔记本的单元格历史(已经键入的单元格)会保留,因此 .save + 仍会将它们保存下来。.reset 之前声明的变量将会消失。 + + + 内核状态已重置。 + + + 重置内核状态;保留单元格历史。 + + + 正在转换为 .verso;使用 --preserve-format 可保留 {0}。 + + + 序列化会话笔记本。省略 <path> 时,保存到最初加载的路径 + (如果有),否则报错。格式根据扩展名推断。未指定 + --preserve-format 时,对加载自 .ipynb 的笔记本执行不带参数的 .save + 会转换为同目录下的 .verso 文件;指定后则保留原格式。 + + + 已将 {0} 保存到 {1} + + + 保存失败:{0} + + + 会话没有已加载的笔记本时,必须提供路径。 + + + 将会话笔记本写入磁盘。 + + + 更新某一项 REPL 运行时设置。 + 已知的键:preview.rows, preview.lines, preview.elapsedThresholdMs, confirmOnExit。 + + + 可尝试 {0} 查看键的列表。 + + + {0} 的 true 或 false 值无效:“{1}” + + + {0} 的整数无效:“{1}” + + + {0} 的非负整数无效:“{1}” + + + 已知的键:{0}。 + + + 设置 REPL 运行时设置(preview.rows、preview.lines、preview.elapsedThresholdMs)。 + + + 未知的设置键“{0}”。 + + + 活动主题:{0} + + + 不带参数时,打印活动主题。 + 带名称时,更改活动主题。按 DisplayName 不区分大小写匹配, + 并以 ThemeId 作为后备。 + + + 打印或切换活动主题。 + + + 已切换到主题:{0} + + + 列出共享 IVariableStore 中的变量。列:名称、类型、预览。 + + + 无变量。 + + + 列出 IVariableStore 中的变量。 + + + 完整打印最后一个单元格(或指定的第 n 个单元格)的输出, + 不受常规渲染时行数上限的限制。 + + + 单元格索引“{0}”无效或超出范围。 + + + 单元格 [{0}] 没有输出。 + + + 没有可查看的单元格。 + + + 完整打印最后一个单元格的输出,不做截断。 + + + 安装单元格导入但环境中没有的 Python 包。只会安装分发名称已知的包;其他情况会被报告。 + + + 扫描其他扩展程序集的目录。 + + + 消息和帮助所用的语言,为下列之一:{0}。默认使用系统语言,否则回退到英语。 + + + Python 单元格使用的 Python 解释器的路径。会覆盖自动发现的结果。 + + + 默认值:{0} + + + 笔记本参数: + + + 无效的参数值: + + + 缺少必需的笔记本参数: + + + 必需 + + + 请使用 --param 提供值,或使用 --interactive 以获得提示。 + + + 未知参数“{0}”未在笔记本元数据中定义。将以字符串形式注入。 + + + 仅 .verso 文件支持参数定义。将把所有 --param 值以无类型字符串形式注入。 + + + 必须提供值。 + + + 错误:{0} + + + 严重错误:{0} + + + 警告:{0} + + + (已调回:请在下方复制粘贴或编辑) + + + <输出:{0},{1} 个字符,二进制> + + + 单元格 {0} + + + … 另有 {0} 行 + + + … 另有 {0} 行 + + + (执行耗时 {0}) + + + 执行失败。 + + + <图像:{0},{1},已保存到 {2}> + + + <图像:{0},解码失败:{1}> + + + ...(另有 {0} 行) + + + ...(另有 {0} 行) + + + … 另有 {0} 行 + + + … 另有 {0} 行 + + + (无参数) + + + 单元格:共 {0} 个,成功 {1} 个,失败 {2} 个 + + + 摘要 + + + 时间:{0} 秒 + + + <输出:{0}> + + + [小组件输出,将在打开笔记本时显示] + + + .verso、.ipynb 或 .dib 文件的路径。省略时,以空白的临时笔记本启动。 + + + 已取消。 + + + {0} 个单元格 + + + {0} 个单元格 + + + 在终端中启动交互式 Verso REPL。 + + + 执行错误:{0} + + + 已加载 {0} 个 + + + 扩展: + + + 键入 {0} 查看命令,键入 {1} 退出。 + + + 内核: + + + 笔记本: + + + 主题: + + + <默认> + + + <无> + + + *临时* + + + 元命令“.{0}”出错:{1} + + + 与 <notebook> 一起使用时,在把控制权交给提示符之前执行所有已加载的单元格。 + + + 提示符历史文件的路径。使用 'none' 可禁用持久化历史。 + + + 第一个单元格的活动内核。与 ILanguageKernel.KernelId 不区分大小写匹配。可在运行时用 .kernel 更改。 + + + 默认布局 ID,作为 ActiveLayoutId 传递给 .export,除非该命令另行覆盖。 + + + 打印可用的内核并退出。 + + + 打印已注册的主题并退出。 + + + 禁用 ANSI 样式。输出为纯 UTF-8 文本。 + + + 强制使用面向行的后备提示符,即使终端支持 PrettyPrompt 也绕过它。 + + + 当加载的笔记本是 .ipynb 时,不带参数的 .save 会写回 .ipynb,而不是转换为 .verso。单元格输出会保留。 + + + 输出渲染所用的活动主题。DisplayName 不区分大小写,并以 ThemeId 作为后备。 + + + 先前的执行失败。 + + + Verso REPL {0} + + + 键入 {0} 查看列表。 + + + 未知的元命令“.{0}”。 + + + 会话中有未保存的单元格。 + + + 请先运行 {0},或再次运行 {1} 以放弃更改。 + + + 用法:{0} + + + Verso CLI:执行、提供服务并转换 Verso 笔记本。 + + + 未处理的错误:{0} + + + .verso、.ipynb 或 .dib 文件的路径。 + + + [{0}/{1}] 单元格 {0} 已在 {2} 秒内完成({3}) + + + 以无界面方式执行笔记本并流式输出单元格结果。 + + + [{0}/{1}] 正在执行单元格 {0}({2})... + + + 无效的单元格选择器“{0}”。请使用从 0 开始的索引或单元格 GUID。 + + + 无效的 --param 格式“{0}”。应为 name=value。 + + + 正在从“{0}”加载第三方扩展。这些扩展在无界面执行时会被自动批准。 + + + 只执行指定的单元格(索引或 GUID)。可重复使用。 + + + 在第一个单元格失败时停止执行。 + + + 将单元格写入标准错误的任何内容视为失败。默认关闭,因为进度条、日志和警告通常由运行正常的程序写在那里。若要让管道对此严格处理,请使用此选项。 + + + 忽略每个单元格的 verso:ui.outputVisibility 和 verso:ui.inputCollapsed 元数据;完整显示所有输出。 + + + 在终端输出中包含 markdown 和 HTML 单元格的内容。 + + + 在 stdin 上提示输入缺少的必需参数,而不是直接失败。 + + + 覆盖笔记本的默认内核。 + + + 输出格式:text、json 或 none。 + + + 将输出写入文件而不是 stdout。未指定格式时意味着 --output json。 + + + 设置笔记本参数(格式:name=value)。可重复使用。 + + + 执行后将更新的输出保存回笔记本文件。 + + + 在终端输出中显示解析后的参数值。 + + + 总执行时间上限,以秒为单位。 + + + 允许加载当前会话期间生成的程序集,无需许可。 + + + 将单元格执行进度打印到 stderr。 + + + 拒绝会话生成的扩展“{0}”。请使用 --trust-local-assemblies 以允许。 + + + 不支持或无效的笔记本格式“{0}”。 + + + 启动时要打开的可选笔记本。 + + + 以本地 Web 服务器的形式启动 Verso Blazor 应用程序。 + + + 启动时不打开浏览器标签页。 + + + 禁用 HTTPS(仅 HTTP)。 + + + 要侦听的 HTTP 端口。 + + + 保存已加载的 .ipynb 笔记本时,写回 .ipynb 而不是转换为 .verso。单元格输出会保留。 + + + 将启动详细信息打印到 stderr。 + + + 按 Ctrl+C 停止。 + + + Verso 正在 {0} 上运行 + + + 无法启动服务器:{0} + + + 扩展:{0} + + + 笔记本:{0} + + + 命令 + + + 说明 + + + 显示名称 + + + 扩展名 + + + 格式 + + + ID + + + 种类 + + + 语言 + + + 名称 + + + 预览 + + + 优先级 + + + 状态 + + + 主题 + + + 类型 + + + 版本 + + \ No newline at end of file diff --git a/src/Verso.Cli/Utilities/CellCount.cs b/src/Verso.Cli/Utilities/CellCount.cs new file mode 100644 index 00000000..37fe9fcb --- /dev/null +++ b/src/Verso.Cli/Utilities/CellCount.cs @@ -0,0 +1,20 @@ +using Verso.Abstractions; +using Verso.Cli.Resources; + +namespace Verso.Cli.Utilities; + +/// +/// Writes out how many cells something holds. +/// +/// +/// The phrase is built here and dropped into the surrounding message as one piece, so a sentence +/// like "Saved three cells to notebook.verso" needs one entry rather than a singular and a plural +/// of the whole sentence. Where the count falls in that sentence is then the translator's to +/// decide, which it has to be: not every language puts it where English does. +/// +internal static class CellCount +{ + /// Describes a count of cells in words the reader's language would use. + public static string Describe(int count) + => string.Format(Plural.Of(count, Strings.Repl_CellCount_One, Strings.Repl_CellCount_Other), count); +} diff --git a/src/Verso.Cli/Utilities/DisplayWidth.cs b/src/Verso.Cli/Utilities/DisplayWidth.cs new file mode 100644 index 00000000..fc0bbac5 --- /dev/null +++ b/src/Verso.Cli/Utilities/DisplayWidth.cs @@ -0,0 +1,82 @@ +using System.Globalization; + +namespace Verso.Cli.Utilities; + +/// +/// Measures and pads text by the number of terminal columns it occupies rather than the number +/// of characters it holds. +/// +/// +/// +/// A terminal draws most characters one column wide, but the characters used by Chinese, +/// Japanese and Korean are drawn two columns wide. counts UTF-16 +/// units, so "概要" measures 2 and occupies 4. Padding a column to a fixed width with +/// therefore overshoots by one column for every wide +/// character in it, and a table that lines up in English comes out ragged in Japanese. +/// +/// +/// The ranges below are the wide and fullwidth blocks of Unicode's East Asian Width property. +/// They are written out rather than read from ICU because CharUnicodeInfo does not expose +/// that property, and a table this small is easier to check than a dependency. +/// +/// +public static class DisplayWidth +{ + /// Number of terminal columns occupies. + public static int Measure(string? text) + { + if (string.IsNullOrEmpty(text)) + return 0; + + var columns = 0; + for (var i = 0; i < text.Length; i++) + { + if (char.IsHighSurrogate(text[i]) && i + 1 < text.Length) + { + columns += IsWide(char.ConvertToUtf32(text[i], text[i + 1])) ? 2 : 1; + i++; + continue; + } + + // A combining mark sits on the character before it and takes no column of its own. + if (CharUnicodeInfo.GetUnicodeCategory(text[i]) == UnicodeCategory.NonSpacingMark) + continue; + + columns += IsWide(text[i]) ? 2 : 1; + } + + return columns; + } + + /// + /// Pads with spaces until it occupies + /// terminal columns. Text already that wide is returned unchanged, as + /// does. + /// + public static string PadRight(string? text, int columns) + { + var value = text ?? string.Empty; + var padding = columns - Measure(value); + return padding > 0 ? value + new string(' ', padding) : value; + } + + private static bool IsWide(int codePoint) => codePoint switch + { + >= 0x1100 and <= 0x115F => true, // Hangul Jamo initial consonants + >= 0x2E80 and <= 0x303E => true, // CJK radicals, Kangxi radicals, CJK symbols + >= 0x3041 and <= 0x33FF => true, // Hiragana, Katakana, Hangul Compatibility Jamo, CJK compatibility + >= 0x3400 and <= 0x4DBF => true, // CJK unified ideographs extension A + >= 0x4E00 and <= 0x9FFF => true, // CJK unified ideographs + >= 0xA000 and <= 0xA4CF => true, // Yi syllables and radicals + >= 0xAC00 and <= 0xD7A3 => true, // Hangul syllables + >= 0xF900 and <= 0xFAFF => true, // CJK compatibility ideographs + >= 0xFE10 and <= 0xFE19 => true, // Vertical forms + >= 0xFE30 and <= 0xFE6F => true, // CJK compatibility forms, small form variants + >= 0xFF00 and <= 0xFF60 => true, // Fullwidth ASCII + >= 0xFFE0 and <= 0xFFE6 => true, // Fullwidth currency and other signs + >= 0x1F300 and <= 0x1F64F => true, // Emoji and pictographs + >= 0x1F900 and <= 0x1F9FF => true, // Supplemental symbols and pictographs + >= 0x20000 and <= 0x3FFFD => true, // CJK unified ideographs extensions B and beyond + _ => false, + }; +} diff --git a/src/Verso.Cli/Utilities/LanguageOption.cs b/src/Verso.Cli/Utilities/LanguageOption.cs new file mode 100644 index 00000000..88213681 --- /dev/null +++ b/src/Verso.Cli/Utilities/LanguageOption.cs @@ -0,0 +1,50 @@ +using System.CommandLine; +using Verso.Cli.Resources; +using Verso.Localization; + +namespace Verso.Cli.Utilities; + +/// +/// The shared --language option. Without it the language comes from +/// VERSO_LANGUAGE, then from the operating system, then English. +/// +public static class LanguageOption +{ + /// + /// The one option instance, registered globally on the root command. + /// + /// + /// Unlike the other shared options, which each command creates its own copy of, this one is + /// global. It has to be accepted before a subcommand name so that verso --language de + /// --help works, and a global option is also the only way for every subcommand to read + /// the same instance out of a parse result. + /// + /// Built on first use rather than in a field initializer. An option holds its description as + /// a finished string, so building this one before has run + /// would freeze the help text in whatever language the machine happens to be set to. + /// + /// + public static Option Instance => _instance ??= new( + VersoCultures.Option, + string.Format(Strings.Option_Language, string.Join(", ", VersoCultures.Supported))); + + private static Option? _instance; + + /// + /// Applies the language named in the raw arguments, before anything is parsed. + /// + /// + /// Command and option descriptions are read while the command tree is built, which happens + /// before the parser has looked at anything, so verso --language de --help only works + /// if the language is settled first. That rules out reading it from a parse result, hence the + /// scan. + /// + /// The interface language moves and the formatting culture does not. A notebook run here + /// writes its results to the console and back into the file, so letting a language option + /// change the decimal separator would change the output rather than translate it. + /// + /// + /// The process arguments. + public static void ApplyFromArguments(string[] args) + => VersoCultures.ApplyUiCulture(VersoCultures.Resolve(VersoCultures.FromArguments(args))); +} diff --git a/src/Verso.Cli/Utilities/Messages.cs b/src/Verso.Cli/Utilities/Messages.cs new file mode 100644 index 00000000..d698387a --- /dev/null +++ b/src/Verso.Cli/Utilities/Messages.cs @@ -0,0 +1,59 @@ +using Spectre.Console; +using Verso.Cli.Resources; + +namespace Verso.Cli.Utilities; + +/// +/// Assembles the lines the CLI prints out of translated sentences. +/// +/// +/// The words and the styling are kept apart on purpose. A translator is handed a sentence with +/// numbered placeholders in it and never sees a markup tag, so there is no tag to drop, mistype, +/// or leave unclosed, and a sentence whose emphasis falls on a different word in another language +/// still works because the placeholder travels with it. +/// +/// Everything substituted in is escaped, because a file path or a kernel's error message can +/// contain a square bracket, which the terminal writer would otherwise read as a style. +/// +/// +internal static class Messages +{ + /// Prefixes a message with the word for something that could not be done. + public static string Error(string message) => string.Format(Strings.Prefix_Error, message); + + /// Prefixes a message with the word for something worth knowing that stopped nothing. + public static string Warning(string message) => string.Format(Strings.Prefix_Warning, message); + + /// Prefixes a message with the words for something that ended the session. + public static string Fatal(string message) => string.Format(Strings.Prefix_Fatal, message); + + /// + /// Fills a translated sentence in with values nobody chose the wording of: a file path, a + /// count, a reason an exception gave. The result is safe to hand to a terminal writer. + /// + public static string Say(string sentence, params object?[] values) + => string.Format( + Markup.Escape(sentence), + values.Select(v => (object)Markup.Escape(v?.ToString() ?? string.Empty)).ToArray()); + + /// + /// Fills a translated sentence in with things typed at a keyboard, drawing each in bold. + /// + /// + /// A command, an option, and a setting name are the same in every language, so they are the + /// one part of a sentence that can be styled without the styling landing on the wrong word + /// once the sentence around them has been rewritten. + /// + public static string Typed(string sentence, params string[] typed) + => string.Format( + Markup.Escape(sentence), + typed.Select(t => (object)$"[bold]{Markup.Escape(t)}[/]").ToArray()); + + /// Draws a whole assembled line in one colour. + /// + /// Colour goes on the line rather than on a word inside it. English puts the verb first, so + /// Saved could be highlighted where it stood; a language that ends with its verb would + /// leave the colour on whatever happened to be first instead. + /// + public static string In(string style, string assembled) => $"[{style}]{assembled}[/]"; +} diff --git a/src/Verso.Cli/Utilities/PythonAutoInstallOption.cs b/src/Verso.Cli/Utilities/PythonAutoInstallOption.cs index 1aa99222..647f674a 100644 --- a/src/Verso.Cli/Utilities/PythonAutoInstallOption.cs +++ b/src/Verso.Cli/Utilities/PythonAutoInstallOption.cs @@ -1,4 +1,5 @@ using System.CommandLine; +using Verso.Cli.Resources; namespace Verso.Cli.Utilities; @@ -12,10 +13,7 @@ public static class PythonAutoInstallOption /// Environment variable the Python kernel reads for its install policy. private const string PolicyVariable = "VERSO_PYTHON_AUTO_INSTALL"; - public static Option Create() => new( - "--auto-install", - "Install Python packages a cell imports but the environment does not have. Only packages " + - "whose distribution name is known are installed; anything else is reported."); + public static Option Create() => new("--auto-install", Strings.Option_AutoInstall); /// /// Publish the policy to the current process so the kernel picks it up when it starts. The diff --git a/src/Verso.Cli/Utilities/PythonInterpreterOption.cs b/src/Verso.Cli/Utilities/PythonInterpreterOption.cs index 9c4c70e6..fb4be99b 100644 --- a/src/Verso.Cli/Utilities/PythonInterpreterOption.cs +++ b/src/Verso.Cli/Utilities/PythonInterpreterOption.cs @@ -1,4 +1,5 @@ using System.CommandLine; +using Verso.Cli.Resources; namespace Verso.Cli.Utilities; @@ -12,9 +13,7 @@ public static class PythonInterpreterOption /// Environment variable the Python kernel consults as its highest-precedence choice. private const string InterpreterVariable = "VERSO_PYTHON"; - public static Option Create() => new( - "--python", - "Path to the Python interpreter used by Python cells. Overrides automatic discovery."); + public static Option Create() => new("--python", Strings.Option_Python); /// /// Publish the selection to the current process so the kernel picks it up when it starts. diff --git a/src/Verso.Cli/Utilities/SerializerResolver.cs b/src/Verso.Cli/Utilities/SerializerResolver.cs index cff3c6fe..bc370780 100644 --- a/src/Verso.Cli/Utilities/SerializerResolver.cs +++ b/src/Verso.Cli/Utilities/SerializerResolver.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Cli.Resources; using Verso.Extensions; namespace Verso.Cli.Utilities; @@ -22,8 +23,8 @@ public static INotebookSerializer Resolve(ExtensionHost extensionHost, string fi if (serializer is null) { var extension = Path.GetExtension(filePath); - throw new SerializerNotFoundException( - $"Unsupported notebook format '{extension}'. Supported formats: .verso, .ipynb, .md, .dib"); + throw new SerializerNotFoundException(string.Format( + Strings.Error_UnsupportedNotebookFormat, extension, ".verso, .ipynb, .md, .dib")); } return serializer; @@ -48,8 +49,8 @@ public static INotebookSerializer ResolveByFormat(ExtensionHost extensionHost, s if (serializer is null) { - throw new SerializerNotFoundException( - $"Unsupported output format '{format}'. Supported formats: verso, ipynb, md, dib"); + throw new SerializerNotFoundException(string.Format( + Strings.Error_UnsupportedOutputFormat, format, "verso, ipynb, md, dib")); } return serializer; diff --git a/src/Verso.Cli/Verso.Cli.csproj b/src/Verso.Cli/Verso.Cli.csproj index 932c569c..b0ada76e 100644 --- a/src/Verso.Cli/Verso.Cli.csproj +++ b/src/Verso.Cli/Verso.Cli.csproj @@ -62,6 +62,21 @@ + + + + MSBuild:Compile + $(IntermediateOutputPath)Strings.Designer.cs + CSharp + Verso.Cli.Resources + Strings + + + diff --git a/src/Verso.FSharp/FSharpExtension.cs b/src/Verso.FSharp/FSharpExtension.cs index 1a1d60e0..04db8452 100644 --- a/src/Verso.FSharp/FSharpExtension.cs +++ b/src/Verso.FSharp/FSharpExtension.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.FSharp.Resources; namespace Verso.FSharp; @@ -14,7 +15,7 @@ public sealed class FSharpExtension : IExtension public string Name => "Verso.FSharp"; public string Version => "1.0.0"; public string? Author => "Datafication"; - public string? Description => "F# Interactive language kernel extension for Verso notebooks."; + public string? Description => Strings.Extension_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; diff --git a/src/Verso.FSharp/Formatters/FSharpDataFormatter.cs b/src/Verso.FSharp/Formatters/FSharpDataFormatter.cs index 8c5a0da2..2d8e422e 100644 --- a/src/Verso.FSharp/Formatters/FSharpDataFormatter.cs +++ b/src/Verso.FSharp/Formatters/FSharpDataFormatter.cs @@ -5,6 +5,7 @@ using Microsoft.FSharp.Core; using Microsoft.FSharp.Reflection; using Verso.Abstractions; +using Verso.FSharp.Resources; namespace Verso.FSharp.Formatters; @@ -23,10 +24,10 @@ public sealed class FSharpDataFormatter : IDataFormatter // --- IExtension --- public string ExtensionId => "verso.fsharp.formatter"; - public string Name => "F# Data Formatter"; + public string Name => Strings.Formatter_Name; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Formats F# types as rich HTML tables and styled output."; + public string? Description => Strings.Formatter_Description; // --- IDataFormatter --- diff --git a/src/Verso.FSharp/Import/JupyterFSharpPostProcessor.cs b/src/Verso.FSharp/Import/JupyterFSharpPostProcessor.cs index cba12712..552bed0a 100644 --- a/src/Verso.FSharp/Import/JupyterFSharpPostProcessor.cs +++ b/src/Verso.FSharp/Import/JupyterFSharpPostProcessor.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; using Verso.Abstractions; +using Verso.FSharp.Resources; namespace Verso.FSharp.Import; @@ -25,10 +26,10 @@ public sealed class JupyterFSharpPostProcessor : INotebookPostProcessor // --- IExtension --- public string ExtensionId => "verso.fsharp.postprocessor.jupyter-fsharp"; - string IExtension.Name => "Jupyter F# Import Hook"; + string IExtension.Name => Strings.Import_Name; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Converts Polyglot Notebooks F# patterns to Verso F# cells on Jupyter import."; + public string? Description => Strings.Import_Description; // --- INotebookPostProcessor --- diff --git a/src/Verso.FSharp/Kernel/FSharpKernel.cs b/src/Verso.FSharp/Kernel/FSharpKernel.cs index 847e4573..b5e8566a 100644 --- a/src/Verso.FSharp/Kernel/FSharpKernel.cs +++ b/src/Verso.FSharp/Kernel/FSharpKernel.cs @@ -10,6 +10,7 @@ using Verso.FSharp.NuGet; using FcsDiagnostic = FSharp.Compiler.Diagnostics.FSharpDiagnostic; +using Verso.FSharp.Resources; namespace Verso.FSharp.Kernel; @@ -53,7 +54,7 @@ internal FSharpKernel(FSharpKernelOptions options) public string Name => "F# (Interactive)"; public string Version => "1.0.0"; public string? Author => "Datafication"; - public string? Description => "F# language kernel powered by FSharp.Compiler.Service."; + public string? Description => Strings.Kernel_Description; // --- ILanguageKernel --- @@ -66,21 +67,25 @@ internal FSharpKernel(FSharpKernelOptions options) // --- IExtensionSettings --- - public IReadOnlyList SettingDefinitions { get; } = new[] + /// + /// Built on each read rather than held, so the settings panel shows the words in the + /// language the reader asked for rather than the one the kernel first loaded in. + /// + public IReadOnlyList SettingDefinitions => new[] { - new SettingDefinition("warningLevel", "Warning Level", - "F# compiler warning level (0\u20135).", + new SettingDefinition("warningLevel", Strings.Setting_WarningLevel_Label, + Strings.Setting_WarningLevel_Description, SettingType.Integer, 3, "Compiler", new SettingConstraints(MinValue: 0, MaxValue: 5)), - new SettingDefinition("langVersion", "Language Version", - "F# language version for the session.", + new SettingDefinition("langVersion", Strings.Setting_LangVersion_Label, + Strings.Setting_LangVersion_Description, SettingType.StringChoice, "preview", "Compiler", new SettingConstraints(Choices: new[] { "default", "latest", "latestmajor", "preview", "5.0", "6.0", "7.0", "8.0", "9.0" })), - new SettingDefinition("publishPrivateBindings", "Publish Private Bindings", - "Whether to publish underscore-prefixed bindings to the variable store.", + new SettingDefinition("publishPrivateBindings", Strings.Setting_PublishPrivateBindings_Label, + Strings.Setting_PublishPrivateBindings_Description, SettingType.Boolean, false, "Variables"), - new SettingDefinition("maxCollectionDisplay", "Max Collection Display", - "Maximum number of collection elements to display in formatted output.", + new SettingDefinition("maxCollectionDisplay", Strings.Setting_MaxCollectionDisplay_Label, + Strings.Setting_MaxCollectionDisplay_Description, SettingType.Integer, 100, "Display", new SettingConstraints(MinValue: 10, MaxValue: 10000)), }; @@ -323,7 +328,7 @@ public async Task> ExecuteAsync(string code, IExecutio { var errorOutput = new CellOutput( "text/plain", - result.CompilationErrorText ?? "Compilation error", + result.CompilationErrorText ?? Strings.Run_CompilationError, IsError: true, ErrorName: "CompilationError"); outputs.Add(errorOutput); @@ -784,7 +789,7 @@ private static CellOutput FormatException(Exception ex) { return new CellOutput( "text/plain", - "Stack overflow. The computation exceeded the stack size limit. Consider restarting the kernel.", + Strings.Run_StackOverflow, IsError: true, ErrorName: "StackOverflowException"); } @@ -793,7 +798,7 @@ private static CellOutput FormatException(Exception ex) { return new CellOutput( "text/plain", - "Out of memory. The computation exceeded available memory. Consider restarting the kernel.", + Strings.Run_OutOfMemory, IsError: true, ErrorName: "OutOfMemoryException"); } @@ -831,7 +836,7 @@ private static string FormatInstalledPackagesHtml(List { var items = string.Join("", packages.Select(p => $"
  • {p.PackageId}, {p.ResolvedVersion}
  • ")); - return $"
    Installed Packages
      {items}
    "; + return $"
    {Strings.Kernel_InstalledPackages}
      {items}
    "; } private async Task TryFormatAsync(object value, IExecutionContext context) diff --git a/src/Verso.FSharp/NuGet/NuGetFallbackResolver.cs b/src/Verso.FSharp/NuGet/NuGetFallbackResolver.cs index 283a71ff..3442c3f5 100644 --- a/src/Verso.FSharp/NuGet/NuGetFallbackResolver.cs +++ b/src/Verso.FSharp/NuGet/NuGetFallbackResolver.cs @@ -6,6 +6,7 @@ using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Versioning; +using Verso.FSharp.Resources; namespace Verso.FSharp.NuGet; @@ -264,9 +265,11 @@ await ResolveWithDependenciesAsync( if (resource is null || resolvedVersion is null) { var sourceNames = string.Join(", ", _sources.Select(s => s.PackageSource.Source)); - var message = $"Package '{packageId}'{(version is not null ? $" v{version}" : "")} was not found on any configured source. Sources tried: {sourceNames}"; + var message = string.Format(Strings.NuGet_PackageNotFound, + packageId, version is not null ? $" v{version}" : "", sourceNames); if (lastException is not null) - message += $" Last error: {lastException.GetType().Name}: {lastException.Message}"; + message += string.Format(Strings.NuGet_PackageNotFound_LastError, + lastException.GetType().Name, lastException.Message); throw new InvalidOperationException(message); } @@ -304,13 +307,14 @@ await ResolveWithDependenciesAsync( if (!downloaded) throw new InvalidOperationException( - $"Failed to download package '{packageId}' v{resolvedVersion} from nuget.org."); + string.Format(Strings.NuGet_DownloadFailed, packageId, resolvedVersion)); } } catch (Exception ex) when (ex is not OperationCanceledException and not InvalidOperationException) { throw new InvalidOperationException( - $"Unable to download package '{packageId}' v{resolvedVersion}. Check your network connection and try again. ({ex.GetType().Name}: {ex.Message})", ex); + string.Format(Strings.NuGet_DownloadUnreachable, + packageId, resolvedVersion, ex.GetType().Name, ex.Message), ex); } var assemblyPaths = new List(); diff --git a/src/Verso.FSharp/Resources/Strings.de.resx b/src/Verso.FSharp/Resources/Strings.de.resx new file mode 100644 index 00000000..e68b9d8f --- /dev/null +++ b/src/Verso.FSharp/Resources/Strings.de.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + F# Interactive-Sprachkernel als Erweiterung für Verso-Notebooks. + + + Formatiert F#-Typen als reichhaltige HTML-Tabellen und gestaltete Ausgabe. + + + F#-Daten-Formatierer + + + Wandelt beim Jupyter-Import die F#-Muster von Polyglot Notebooks in Verso-F#-Zellen um. + + + Jupyter-F#-Import-Hook + + + F#-Sprachkernel auf Basis von FSharp.Compiler.Service. + + + Installierte Pakete + + + Das Paket '{0}' v{1} konnte nicht von nuget.org heruntergeladen werden. + + + Das Paket '{0}' v{1} konnte nicht heruntergeladen werden. Prüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut. ({2}: {3}) + + + Das Paket '{0}'{1} wurde in keiner konfigurierten Quelle gefunden. Geprüfte Quellen: {2} + + + Letzter Fehler: {0}: {1} + + + Kompilierungsfehler + + + Kein Speicher mehr. Die Berechnung hat den verfügbaren Speicher überschritten. Starten Sie den Kernel gegebenenfalls neu. + + + Stapelüberlauf. Die Berechnung hat die Grenze der Stapelgröße überschritten. Starten Sie den Kernel gegebenenfalls neu. + + + F#-Sprachversion für die Sitzung. + + + Sprachversion + + + Höchstzahl der Auflistungselemente, die in der formatierten Ausgabe angezeigt werden. + + + Max. Auflistungsanzeige + + + Ob Bindungen mit vorangestelltem Unterstrich im Variablenspeicher veröffentlicht werden. + + + Private Bindungen veröffentlichen + + + Warnstufe des F#-Compilers (0–5). + + + Warnstufe + + \ No newline at end of file diff --git a/src/Verso.FSharp/Resources/Strings.es.resx b/src/Verso.FSharp/Resources/Strings.es.resx new file mode 100644 index 00000000..b3dcfafd --- /dev/null +++ b/src/Verso.FSharp/Resources/Strings.es.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Extensión del kernel de lenguaje F# Interactive para los cuadernos de Verso. + + + Da formato a los tipos de F# como tablas HTML enriquecidas y salida con estilo. + + + Formateador de datos de F# + + + Convierte los patrones F# de Polyglot Notebooks en celdas F# de Verso al importar desde Jupyter. + + + Enlace de importación de F# para Jupyter + + + Kernel del lenguaje F# basado en FSharp.Compiler.Service. + + + Paquetes instalados + + + No se pudo descargar el paquete '{0}' v{1} desde nuget.org. + + + No se puede descargar el paquete '{0}' v{1}. Compruebe la conexión de red e inténtelo de nuevo. ({2}: {3}) + + + No se encontró el paquete '{0}'{1} en ningún origen configurado. Orígenes probados: {2} + + + Último error: {0}: {1} + + + Error de compilación + + + Memoria insuficiente. El cálculo superó la memoria disponible. Considere reiniciar el kernel. + + + Desbordamiento de pila. El cálculo superó el límite de tamaño de la pila. Considere reiniciar el kernel. + + + Versión del lenguaje F# de la sesión. + + + Versión del lenguaje + + + Número máximo de elementos de una colección que se muestran en la salida con formato. + + + Máximo de colección + + + Si se publican en el almacén de variables los enlaces cuyo nombre empieza por guion bajo. + + + Publicar enlaces privados + + + Nivel de advertencia del compilador de F# (0–5). + + + Nivel de advertencia + + \ No newline at end of file diff --git a/src/Verso.FSharp/Resources/Strings.ja.resx b/src/Verso.FSharp/Resources/Strings.ja.resx new file mode 100644 index 00000000..99275e6d --- /dev/null +++ b/src/Verso.FSharp/Resources/Strings.ja.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Verso ノートブック向けの F# Interactive 言語カーネル拡張機能。 + + + F# の型を、見やすい HTML の表とスタイル付きの出力として整形します。 + + + F# データフォーマッター + + + Jupyter のインポート時に、Polyglot Notebooks の F# の書き方を Verso の F# セルに変換します。 + + + Jupyter F# インポートフック + + + FSharp.Compiler.Service を利用した F# 言語カーネル。 + + + インストールされたパッケージ + + + nuget.org からパッケージ '{0}' v{1} をダウンロードできませんでした。 + + + パッケージ '{0}' v{1} をダウンロードできません。ネットワーク接続を確認して、もう一度試してください。({2}: {3}) + + + パッケージ '{0}'{1} は、設定されているどのソースにも見つかりませんでした。試したソース: {2} + + + 最後のエラー: {0}: {1} + + + コンパイルエラー + + + メモリが不足しました。計算に使えるメモリを超えています。カーネルの再起動を検討してください。 + + + スタックオーバーフローが発生しました。計算がスタックサイズの上限を超えています。カーネルの再起動を検討してください。 + + + このセッションで使う F# の言語バージョン。 + + + 言語バージョン + + + 整形された出力に表示するコレクションの要素数の上限。 + + + コレクションの表示上限 + + + アンダースコアで始まる束縛を変数ストアに公開するかどうか。 + + + プライベートな束縛を公開 + + + F# コンパイラの警告レベル (0–5)。 + + + 警告レベル + + \ No newline at end of file diff --git a/src/Verso.FSharp/Resources/Strings.qps-Ploc.resx b/src/Verso.FSharp/Resources/Strings.qps-Ploc.resx new file mode 100644 index 00000000..bc06589d --- /dev/null +++ b/src/Verso.FSharp/Resources/Strings.qps-Ploc.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + [!!F# Ïñtéràçtïvé làñgùàgé kérñél éxtéñšïòñ fòr Véršò ñòtébòòkš.···!!] + + + [!!Fòrmàtš F# tÿpéš àš rïçh HTML tàbléš àñd štÿléd òùtpùt.···!!] + + + [!!F# Dàtà Fòrmàttér···!!] + + + [!!Çòñvértš Pòlÿglòt Ñòtébòòkš F# pàttérñš tò Véršò F# çéllš òñ Jùpÿtér ïmpòrt.···!!] + + + [!!Jùpÿtér F# Ïmpòrt Hòòk···!!] + + + [!!F# làñgùàgé kérñél pòwéréd bÿ FŠhàrp.Çòmpïlér.Šérvïçé.···!!] + + + [!!Ïñštàlléd Pàçkàgéš···!!] + + + [!!Fàïléd tò dòwñlòàd pàçkàgé '{0}' v{1} fròm ñùgét.òrg.···!!] + + + [!!Ùñàblé tò dòwñlòàd pàçkàgé '{0}' v{1}. Çhéçk ÿòùr ñétwòrk çòññéçtïòñ àñd trÿ àgàïñ. ({2}: {3})···!!] + + + [!!Pàçkàgé '{0}'{1} wàš ñòt fòùñd òñ àñÿ çòñfïgùréd šòùrçé. Šòùrçéš trïéd: {2}···!!] + + + [!! Làšt érròr: {0}: {1}···!!] + + + [!!Çòmpïlàtïòñ érròr···!!] + + + [!!Òùt òf mémòrÿ. Thé çòmpùtàtïòñ éxçéédéd àvàïlàblé mémòrÿ. Çòñšïdér réštàrtïñg thé kérñél.···!!] + + + [!!Štàçk òvérflòw. Thé çòmpùtàtïòñ éxçéédéd thé štàçk šïzé lïmït. Çòñšïdér réštàrtïñg thé kérñél.···!!] + + + [!!F# làñgùàgé véršïòñ fòr thé šéššïòñ.···!!] + + + [!!Làñgùàgé Véršïòñ···!!] + + + [!!Màxïmùm ñùmbér òf çòlléçtïòñ éléméñtš tò dïšplàÿ ïñ fòrmàttéd òùtpùt.···!!] + + + [!!Màx Çòlléçtïòñ Dïšplàÿ···!!] + + + [!!Whéthér tò pùblïšh ùñdéršçòré-préfïxéd bïñdïñgš tò thé vàrïàblé štòré.···!!] + + + [!!Pùblïšh Prïvàté Bïñdïñgš···!!] + + + [!!F# çòmpïlér wàrñïñg lévél (0–5).···!!] + + + [!!Wàrñïñg Lévél···!!] + + \ No newline at end of file diff --git a/src/Verso.FSharp/Resources/Strings.resx b/src/Verso.FSharp/Resources/Strings.resx new file mode 100644 index 00000000..87eb440a --- /dev/null +++ b/src/Verso.FSharp/Resources/Strings.resx @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + F# Interactive language kernel extension for Verso notebooks. + What this extension is, shown wherever extensions are listed. + + + Formats F# types as rich HTML tables and styled output. + What this formatter does, shown wherever extensions are listed. + + + F# Data Formatter + Name of the formatter for F# values such as lists, records and maps, as listed in the Extensions panel. F# is a language name. + + + Converts Polyglot Notebooks F# patterns to Verso F# cells on Jupyter import. + What this import hook does, shown wherever extensions are listed. + + + Jupyter F# Import Hook + Name of the hook that turns F# cells in an imported Jupyter notebook into Verso F# cells, as listed in the Extensions panel. + + + F# language kernel powered by FSharp.Compiler.Service. + What the F# kernel is, shown wherever kernels are listed. FSharp.Compiler.Service is the name of the library and stays as written. + + + Installed Packages + Heading above the list of packages a cell installed. Keep it short; it sits above a list. + + + Failed to download package '{0}' v{1} from nuget.org. + {0} is a package name and {1} a version number. nuget.org is an address and stays as written. + + + Unable to download package '{0}' v{1}. Check your network connection and try again. ({2}: {3}) + {0} is a package name, {1} a version number, {2} the kind of failure and {3} its message. The last two arrive in English. + + + Package '{0}'{1} was not found on any configured source. Sources tried: {2} + {0} is a package name, {1} is either empty or a version written as ' v1.2.3', and {2} is a comma-separated list of addresses. + + + Last error: {0}: {1} + Added to the end of the message above when a source answered with a failure. It opens with a space because it follows a full stop. {0} is the kind of failure and {1} its message, both of which arrive in English. + + + Compilation error + Shown in place of the compiler's own message when it would not compile and said nothing about why. + + + Out of memory. The computation exceeded available memory. Consider restarting the kernel. + A cell asked for more memory than there was. Shown as the cell's error. + + + Stack overflow. The computation exceeded the stack size limit. Consider restarting the kernel. + A cell recursed too deeply. Shown as the cell's error. + + + F# language version for the session. + What the setting above does. + + + Language Version + Name of a setting. Keep it short. + + + Maximum number of collection elements to display in formatted output. + What the setting above does. + + + Max Collection Display + Name of a setting. Keep it short. + + + Whether to publish underscore-prefixed bindings to the variable store. + What the setting above does. A binding whose name starts with an underscore is F#'s way of marking it private. + + + Publish Private Bindings + Name of a setting. Keep it short. + + + F# compiler warning level (0–5). + What the setting above does. The range is a pair of numbers and stays as written. + + + Warning Level + Name of a setting, shown in the settings panel. Keep it short; it sits on one line beside a field. + + \ No newline at end of file diff --git a/src/Verso.FSharp/Resources/Strings.zh-Hans.resx b/src/Verso.FSharp/Resources/Strings.zh-Hans.resx new file mode 100644 index 00000000..bf3ebacc --- /dev/null +++ b/src/Verso.FSharp/Resources/Strings.zh-Hans.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 适用于 Verso 笔记本的 F# Interactive 语言内核扩展。 + + + 将 F# 类型格式化为富 HTML 表格和带样式的输出。 + + + F# 数据格式化程序 + + + 在导入 Jupyter 文件时,将 Polyglot Notebooks 的 F# 模式转换为 Verso F# 单元格。 + + + Jupyter F# 导入挂钩 + + + 由 FSharp.Compiler.Service 驱动的 F# 语言内核。 + + + 已安装的包 + + + 无法从 nuget.org 下载包“{0}”v{1}。 + + + 无法下载包“{0}”v{1}。请检查网络连接后重试。({2}:{3}) + + + 在任何已配置的源中都找不到包“{0}”{1}。已尝试的源:{2} + + + 上一个错误:{0}:{1} + + + 编译错误 + + + 内存不足。计算超出了可用内存。请考虑重启内核。 + + + 堆栈溢出。计算超出了堆栈大小限制。请考虑重启内核。 + + + 此会话使用的 F# 语言版本。 + + + 语言版本 + + + 格式化输出中显示的集合元素的最大数量。 + + + 集合显示上限 + + + 是否将以下划线开头的绑定发布到变量存储。 + + + 发布私有绑定 + + + F# 编译器警告级别(0–5)。 + + + 警告级别 + + \ No newline at end of file diff --git a/src/Verso.FSharp/Verso.FSharp.csproj b/src/Verso.FSharp/Verso.FSharp.csproj index 9940a660..8767093e 100644 --- a/src/Verso.FSharp/Verso.FSharp.csproj +++ b/src/Verso.FSharp/Verso.FSharp.csproj @@ -15,6 +15,23 @@ + + + + MSBuild:Compile + $(IntermediateOutputPath)Strings.Designer.cs + CSharp + Verso.FSharp.Resources + Strings + + + diff --git a/src/Verso.Host/Handlers/DiffHandler.cs b/src/Verso.Host/Handlers/DiffHandler.cs index 1e359b5d..2ef481d5 100644 --- a/src/Verso.Host/Handlers/DiffHandler.cs +++ b/src/Verso.Host/Handlers/DiffHandler.cs @@ -4,6 +4,7 @@ using Verso.Host.Dto; using Verso.Host.Protocol; using Verso.Serializers; +using Verso.Host.Resources; namespace Verso.Host.Handlers; @@ -22,7 +23,7 @@ public static async Task HandleDiffAsync(NotebookSession ns, if (string.IsNullOrWhiteSpace(p.BaselineContent)) { - throw new InvalidOperationException("The baseline notebook is empty."); + throw new InvalidOperationException(Strings.Diff_BaselineEmpty); } // Serializer selection mirrors notebook/open: file-path extension first, then a @@ -48,7 +49,7 @@ public static async Task HandleDiffAsync(NotebookSession ns, } catch (Exception ex) { - throw new InvalidOperationException($"Could not parse the baseline as a notebook: {ex.Message}", ex); + throw new InvalidOperationException(string.Format(Strings.Diff_BaselineUnreadable, ex.Message), ex); } var postProcessors = ns.ExtensionHost.GetPostProcessors() diff --git a/src/Verso.Host/Handlers/ExtensionHandler.cs b/src/Verso.Host/Handlers/ExtensionHandler.cs index 1f6fa4af..01b36984 100644 --- a/src/Verso.Host/Handlers/ExtensionHandler.cs +++ b/src/Verso.Host/Handlers/ExtensionHandler.cs @@ -4,6 +4,7 @@ using Verso.Extensions.Marketplace; using Verso.Host.Dto; using Verso.Host.Protocol; +using Verso.Host.Resources; namespace Verso.Host.Handlers; @@ -67,10 +68,10 @@ public static async Task HandleInstallAsync(NotebookSess { if (!TrustStore.IsApproved(p.PackageId, p.Version)) { - var consent = new[] { new ExtensionConsentInfo(p.PackageId, p.Version, "marketplace") }; + var consent = new[] { new ExtensionConsentInfo(p.PackageId, p.Version, Verso.Resources.Strings.Consent_Source_Marketplace) }; var approved = await ns.ExtensionHost.RequestExtensionConsentAsync(consent, CancellationToken.None); if (!approved) - return new ExtensionInstallResult { Success = false, ErrorMessage = "Installation was not approved." }; + return new ExtensionInstallResult { Success = false, ErrorMessage = Strings.Extension_NotApproved }; TrustStore.Approve(p.PackageId, p.Version); TrustStore.Save(); @@ -115,7 +116,7 @@ public static async Task HandleInstallLocalAsync(Noteboo ?? throw new JsonException("Missing params for extension/installLocal"); if (string.IsNullOrWhiteSpace(p.Path)) - return new ExtensionInstallResult { Success = false, ErrorMessage = "No file path was provided." }; + return new ExtensionInstallResult { Success = false, ErrorMessage = Strings.Extension_NoFilePath }; try { diff --git a/src/Verso.Host/Handlers/LayoutHandler.cs b/src/Verso.Host/Handlers/LayoutHandler.cs index a7aa863d..e41276ee 100644 --- a/src/Verso.Host/Handlers/LayoutHandler.cs +++ b/src/Verso.Host/Handlers/LayoutHandler.cs @@ -3,9 +3,21 @@ using Verso.Host.Dto; using Verso.Host.Layouts; using Verso.Host.Protocol; +using Verso.Host.Resources; namespace Verso.Host.Handlers; +/// +/// Answers the layout half of the protocol: which layouts exist, which one is active, and what an +/// isolated one needs in order to draw. +/// +/// +/// The messages naming a missing field or an unknown identifier stay in English on purpose. They +/// can only appear when a caller sends a request this cannot read, which is a fault in the code +/// rather than anything the reader did, and a log from one machine has to match a search made on +/// another. The same goes for the messages an extension gets back when it uses a layout the wrong +/// way: the person who can act on one is writing code, not reading a notebook. +/// public static class LayoutHandler { public static LayoutsResult HandleGetLayouts(NotebookSession ns) @@ -575,7 +587,7 @@ public Task RequestFileDownloadAsync(string fileName, string contentType, byte[] // Reuse the same file/download path the toolbar export actions use, so a layout // interaction (e.g. export PNG) gets a native save dialog in the VS Code host. if (_ns is null) - throw new NotSupportedException("File download is not supported by this host context."); + throw new NotSupportedException(Strings.Download_Unsupported); _ns.SendNotification(MethodNames.FileDownload, new { diff --git a/src/Verso.Host/Handlers/NotebookHandler.cs b/src/Verso.Host/Handlers/NotebookHandler.cs index aa3408bd..558910aa 100644 --- a/src/Verso.Host/Handlers/NotebookHandler.cs +++ b/src/Verso.Host/Handlers/NotebookHandler.cs @@ -380,7 +380,12 @@ private static bool LanguageSupportsCancellation(string languageId) => public static CellTypesResult HandleGetCellTypes(NotebookSession ns) { - var types = new List { new() { Id = "code", DisplayName = "Code" } }; + // Code has no ICellType of its own: it is what a cell is when nothing else claims it, + // so the name comes from the engine's resources rather than a registration. + var types = new List + { + new() { Id = "code", DisplayName = Verso.Resources.Strings.CellType_Code } + }; var extHost = ns.ExtensionHost; diff --git a/src/Verso.Host/Handlers/ParameterHandler.cs b/src/Verso.Host/Handlers/ParameterHandler.cs index b74a362d..b377c2a3 100644 --- a/src/Verso.Host/Handlers/ParameterHandler.cs +++ b/src/Verso.Host/Handlers/ParameterHandler.cs @@ -3,11 +3,17 @@ using Verso.Host.Dto; using Verso.Host.Protocol; using Verso.Parameters; +using Verso.Host.Resources; namespace Verso.Host.Handlers; public static class ParameterHandler { + // Two kinds of message live here. A request arriving without the fields it declares is a + // fault in the caller and reads the same in every language, so a log matches a search. A name + // the reader already used, or a default that does not fit the type they chose, is something + // they can act on, and is translated. + public static ParameterListResult HandleList(NotebookSession ns) { var parameters = ns.Scaffold.Notebook.Parameters; @@ -29,20 +35,20 @@ public static object HandleAdd(NotebookSession ns, JsonElement? @params) ?? throw new JsonException("Missing params for parameter/add"); if (string.IsNullOrEmpty(p.Name)) - throw new JsonException("Parameter name is required."); + throw new JsonException(Strings.Parameter_NameRequired); var notebook = ns.Scaffold.Notebook; notebook.Parameters ??= new Dictionary(); if (notebook.Parameters.ContainsKey(p.Name)) - throw new InvalidOperationException($"Parameter '{p.Name}' already exists."); + throw new InvalidOperationException(string.Format(Strings.Parameter_AlreadyExists, p.Name)); var typeId = p.Type ?? "string"; object? defaultValue = null; if (!string.IsNullOrEmpty(p.DefaultValue)) { if (!ParameterValueParser.TryParse(typeId, p.DefaultValue, out var parsed, out var error)) - throw new InvalidOperationException($"Invalid default value: {error}"); + throw new InvalidOperationException(string.Format(Strings.Parameter_InvalidDefault, error)); defaultValue = parsed; } @@ -76,11 +82,11 @@ public static object HandleUpdate(NotebookSession ns, JsonElement? @params) ?? throw new JsonException("Missing params for parameter/update"); if (string.IsNullOrEmpty(p.Name)) - throw new JsonException("Parameter name is required."); + throw new JsonException(Strings.Parameter_NameRequired); var parameters = ns.Scaffold.Notebook.Parameters; if (parameters is null || !parameters.TryGetValue(p.Name, out var def)) - throw new InvalidOperationException($"Parameter '{p.Name}' not found."); + throw new InvalidOperationException(string.Format(Strings.Parameter_NotFound, p.Name)); if (p.Type is not null) def.Type = p.Type; @@ -94,7 +100,7 @@ public static object HandleUpdate(NotebookSession ns, JsonElement? @params) if (p.DefaultValue is not null) { if (!ParameterValueParser.TryParse(def.Type, p.DefaultValue, out var parsed, out var error)) - throw new InvalidOperationException($"Invalid default value: {error}"); + throw new InvalidOperationException(string.Format(Strings.Parameter_InvalidDefault, error)); def.Default = parsed; ns.Scaffold.Variables.Set(p.Name, parsed!); @@ -111,11 +117,11 @@ public static object HandleRemove(NotebookSession ns, JsonElement? @params) ?? throw new JsonException("Missing params for parameter/remove"); if (string.IsNullOrEmpty(p.Name)) - throw new JsonException("Parameter name is required."); + throw new JsonException(Strings.Parameter_NameRequired); var parameters = ns.Scaffold.Notebook.Parameters; if (parameters is null || !parameters.Remove(p.Name)) - throw new InvalidOperationException($"Parameter '{p.Name}' not found."); + throw new InvalidOperationException(string.Format(Strings.Parameter_NotFound, p.Name)); ns.Scaffold.Variables.Remove(p.Name); InvalidateParametersCell(ns); diff --git a/src/Verso.Host/Program.cs b/src/Verso.Host/Program.cs index c16a01af..a894ac35 100644 --- a/src/Verso.Host/Program.cs +++ b/src/Verso.Host/Program.cs @@ -3,12 +3,18 @@ using System.Threading.Channels; using Verso.Host; using Verso.Host.Protocol; +using Verso.Localization; // Force UTF-8 for stdin/stdout — Windows defaults to the OEM code page (e.g. CP437) // which corrupts non-ASCII characters in JSON-RPC messages. Console.InputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8; +// Before any kernel loads, so a message produced while an extension is being discovered is +// already in the caller's language. Only the interface language moves: this process runs the +// kernels, and a cell's own results are data rather than chrome. +VersoCultures.ApplyUiCulture(VersoCultures.Resolve(VersoCultures.FromArguments(args))); + // Bind the protocol writer to the real underlying stdout stream, not Console.Out. // Kernels (CSharpKernel, FsiSessionManager) call Console.SetOut to a StringWriter // during cell evaluation so they can capture user stdout. If notifications routed diff --git a/src/Verso.Host/Resources/Strings.de.resx b/src/Verso.Host/Resources/Strings.de.resx new file mode 100644 index 00000000..e800fdc2 --- /dev/null +++ b/src/Verso.Host/Resources/Strings.de.resx @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Das Basis-Notebook ist leer. + + + Die Basis konnte nicht als Notebook gelesen werden: {0} + + + Dateidownload wird in dieser Umgebung nicht unterstützt. + + + Es wurde kein Dateipfad angegeben. + + + Die Installation wurde nicht bestätigt. + + + Der Parameter '{0}' ist bereits vorhanden. + + + Ungültiger Standardwert: {0} + + + Ein Parametername ist erforderlich. + + + Der Parameter '{0}' wurde nicht gefunden. + + \ No newline at end of file diff --git a/src/Verso.Host/Resources/Strings.es.resx b/src/Verso.Host/Resources/Strings.es.resx new file mode 100644 index 00000000..1c6f7b79 --- /dev/null +++ b/src/Verso.Host/Resources/Strings.es.resx @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + El cuaderno de la línea base está vacío. + + + No se pudo analizar la línea base como cuaderno: {0} + + + Este contexto de host no admite la descarga de archivos. + + + No se indicó ninguna ruta de archivo. + + + No se aprobó la instalación. + + + El parámetro '{0}' ya existe. + + + Valor predeterminado no válido: {0} + + + El nombre del parámetro es obligatorio. + + + No se encontró el parámetro '{0}'. + + \ No newline at end of file diff --git a/src/Verso.Host/Resources/Strings.ja.resx b/src/Verso.Host/Resources/Strings.ja.resx new file mode 100644 index 00000000..56d2aef2 --- /dev/null +++ b/src/Verso.Host/Resources/Strings.ja.resx @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ベースラインのノートブックが空です。 + + + ベースラインをノートブックとして解析できませんでした: {0} + + + このホストではファイルのダウンロードはサポートされていません。 + + + ファイルパスが指定されていません。 + + + インストールは許可されませんでした。 + + + パラメーター '{0}' は既に存在します。 + + + 既定値が正しくありません: {0} + + + パラメーターの名前は必須です。 + + + パラメーター '{0}' が見つかりません。 + + \ No newline at end of file diff --git a/src/Verso.Host/Resources/Strings.qps-Ploc.resx b/src/Verso.Host/Resources/Strings.qps-Ploc.resx new file mode 100644 index 00000000..e8a587ee --- /dev/null +++ b/src/Verso.Host/Resources/Strings.qps-Ploc.resx @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + [!!Thé bàšélïñé ñòtébòòk ïš émptÿ.···!!] + + + [!!Çòùld ñòt pàršé thé bàšélïñé àš à ñòtébòòk: {0}···!!] + + + [!!Fïlé dòwñlòàd ïš ñòt šùppòrtéd bÿ thïš hòšt çòñtéxt.···!!] + + + [!!Ñò fïlé pàth wàš pròvïdéd.···!!] + + + [!!Ïñštàllàtïòñ wàš ñòt àppròvéd.···!!] + + + [!!Pàràmétér '{0}' àlréàdÿ éxïštš.···!!] + + + [!!Ïñvàlïd défàùlt vàlùé: {0}···!!] + + + [!!Pàràmétér ñàmé ïš réqùïréd.···!!] + + + [!!Pàràmétér '{0}' ñòt fòùñd.···!!] + + \ No newline at end of file diff --git a/src/Verso.Host/Resources/Strings.resx b/src/Verso.Host/Resources/Strings.resx new file mode 100644 index 00000000..c5191d21 --- /dev/null +++ b/src/Verso.Host/Resources/Strings.resx @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The baseline notebook is empty. + The reader chose something to compare against that holds nothing. + + + Could not parse the baseline as a notebook: {0} + The reader chose something to compare against that is not a notebook. {0} is the underlying error, which arrives in English. + + + File download is not supported by this host context. + The reader exported something somewhere that cannot save a file. + + + No file path was provided. + An extension was asked to be installed from a file with no file named. + + + Installation was not approved. + The reader declined the dialog asking whether to install an extension. + + + Parameter '{0}' already exists. + The reader gave a new parameter a name the notebook already uses. {0} is that name. + + + Invalid default value: {0} + The default the reader typed does not fit the parameter's type. {0} says why, and is itself translated. + + + Parameter name is required. + The reader confirmed a new parameter without naming it. + + + Parameter '{0}' not found. + The parameter being changed is no longer in the notebook. {0} is its name. + + \ No newline at end of file diff --git a/src/Verso.Host/Resources/Strings.zh-Hans.resx b/src/Verso.Host/Resources/Strings.zh-Hans.resx new file mode 100644 index 00000000..4ccc7034 --- /dev/null +++ b/src/Verso.Host/Resources/Strings.zh-Hans.resx @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 基线笔记本为空。 + + + 无法将基线解析为笔记本:{0} + + + 此宿主上下文不支持文件下载。 + + + 未提供文件路径。 + + + 安装未获批准。 + + + 参数“{0}”已存在。 + + + 默认值无效:{0} + + + 必须提供参数名称。 + + + 找不到参数“{0}”。 + + \ No newline at end of file diff --git a/src/Verso.Host/Verso.Host.csproj b/src/Verso.Host/Verso.Host.csproj index b47dd887..99c25f44 100644 --- a/src/Verso.Host/Verso.Host.csproj +++ b/src/Verso.Host/Verso.Host.csproj @@ -11,6 +11,23 @@ Verso.Host + + + + MSBuild:Compile + $(IntermediateOutputPath)Strings.Designer.cs + CSharp + Verso.Host.Resources + Strings + + + diff --git a/src/Verso.Http/CellType/HttpCellRenderer.cs b/src/Verso.Http/CellType/HttpCellRenderer.cs index 8adcf1b1..e284ae75 100644 --- a/src/Verso.Http/CellType/HttpCellRenderer.cs +++ b/src/Verso.Http/CellType/HttpCellRenderer.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Http.Resources; namespace Verso.Http.CellType; @@ -11,10 +12,10 @@ public sealed class HttpCellRenderer : ICellRenderer // --- IExtension --- public string ExtensionId => "verso.http.renderer.http"; - public string Name => "HTTP Renderer"; + public string Name => Strings.Renderer_Name; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Renders HTTP request cells."; + public string? Description => Strings.Renderer_Description; // --- ICellRenderer --- diff --git a/src/Verso.Http/CellType/HttpCellType.cs b/src/Verso.Http/CellType/HttpCellType.cs index 48e15a0d..35d8fcdc 100644 --- a/src/Verso.Http/CellType/HttpCellType.cs +++ b/src/Verso.Http/CellType/HttpCellType.cs @@ -1,5 +1,6 @@ using Verso.Abstractions; using Verso.Http.Kernel; +using Verso.Http.Resources; namespace Verso.Http.CellType; @@ -13,10 +14,10 @@ public sealed class HttpCellType : ICellType // --- IExtension --- public string ExtensionId => "verso.http.celltype.http"; - public string Name => "HTTP Cell Type"; + public string Name => Strings.CellType_Name; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "HTTP request cell type for sending REST API requests."; + public string? Description => Strings.CellType_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; diff --git a/src/Verso.Http/Formatting/HttpResponseFormatter.cs b/src/Verso.Http/Formatting/HttpResponseFormatter.cs index dd6868d3..6dcf9b9d 100644 --- a/src/Verso.Http/Formatting/HttpResponseFormatter.cs +++ b/src/Verso.Http/Formatting/HttpResponseFormatter.cs @@ -4,6 +4,7 @@ using System.Xml; using System.Xml.Linq; using Verso.Http.Models; +using Verso.Http.Resources; namespace Verso.Http.Formatting; @@ -76,7 +77,9 @@ internal static string FormatResponseHtml(HttpResponseData response, bool includ if (response.Headers.Count > 0) { sb.Append("
    "); - sb.Append("Response Headers (").Append(response.Headers.Count).Append(")"); + sb.Append("") + .Append(WebUtility.HtmlEncode(string.Format(Strings.Response_Headers, response.Headers.Count))) + .Append(""); sb.Append(""); foreach (var (name, value) in response.Headers) { @@ -108,9 +111,10 @@ internal static string FormatResponseHtml(HttpResponseData response, bool includ if (truncated) { - sb.Append("
    Response truncated at 100 KB (total: ") - .Append((response.Body.Length / 1024).ToString("N0")) - .Append(" KB)
    "); + sb.Append("
    ") + .Append(WebUtility.HtmlEncode(string.Format( + Strings.Response_Truncated, (response.Body.Length / 1024).ToString("N0")))) + .Append("
    "); } sb.Append(""); diff --git a/src/Verso.Http/Kernel/HttpKernel.cs b/src/Verso.Http/Kernel/HttpKernel.cs index ddc81f40..3598a6ca 100644 --- a/src/Verso.Http/Kernel/HttpKernel.cs +++ b/src/Verso.Http/Kernel/HttpKernel.cs @@ -6,6 +6,8 @@ using Verso.Http.Formatting; using Verso.Http.Models; using Verso.Http.Parsing; +using Verso.Http.Localization; +using Verso.Http.Resources; namespace Verso.Http.Kernel; @@ -43,10 +45,10 @@ public sealed class HttpKernel : ILanguageKernel // --- IExtension --- public string ExtensionId => "verso.http.kernel.http"; - string IExtension.Name => "HTTP Kernel"; + string IExtension.Name => Strings.Kernel_Name; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Executes HTTP requests using .http file syntax."; + public string? Description => Strings.Kernel_Description; // --- ILanguageKernel --- public string LanguageId => "http"; @@ -66,7 +68,7 @@ public async Task> ExecuteAsync(string code, IExecutio if (requests.Count == 0) { - outputs.Add(new CellOutput("text/plain", "No HTTP request found.", IsError: true)); + outputs.Add(new CellOutput("text/plain", Strings.Run_NoRequest, IsError: true)); return outputs; } @@ -108,7 +110,8 @@ public async Task> ExecuteAsync(string code, IExecutio else { outputs.Add(new CellOutput("text/plain", - $"Error: Relative URL '{url}' requires a base URL. Use #!http-set-base.", IsError: true)); + CellText.Error(string.Format(Strings.Run_RelativeUrlNeedsBase, url)), + IsError: true)); continue; } } @@ -135,7 +138,7 @@ public async Task> ExecuteAsync(string code, IExecutio // Stream status message await context.WriteOutputAsync(new CellOutput("text/plain", - $"Sending {request.Method} {url}...")).ConfigureAwait(false); + string.Format(Strings.Run_Sending, request.Method, url))).ConfigureAwait(false); // Send request var client = request.NoRedirect ? NoRedirectClient : SharedClient; @@ -193,7 +196,7 @@ await context.WriteOutputAsync(new CellOutput("text/plain", { outputs.Add(new CellOutput( "text/plain", - $"Request failed with status {(int)response.StatusCode} {response.ReasonPhrase}.", + string.Format(Strings.Run_RequestFailed, (int)response.StatusCode, response.ReasonPhrase), IsError: true, ErrorName: "HttpRequestFailed")); } @@ -202,20 +205,20 @@ await context.WriteOutputAsync(new CellOutput("text/plain", { sw.Stop(); outputs.Add(new CellOutput("text/plain", - $"Error: Request timed out after {timeoutSeconds}s.", IsError: true)); + CellText.Error(string.Format(Strings.Run_TimedOut, timeoutSeconds)), IsError: true)); } catch (TaskCanceledException) { sw.Stop(); // The user stopped the cell. Cancellation is its own status everywhere else in // Verso rather than a failure, so this says what happened without claiming one. - outputs.Add(CellOutput.Plain("Request cancelled.")); + outputs.Add(CellOutput.Plain(Strings.Run_Cancelled)); } catch (HttpRequestException ex) { sw.Stop(); outputs.Add(new CellOutput("text/plain", - $"Error: {ex.Message}", IsError: true)); + CellText.Error(ex.Message), IsError: true)); } } @@ -252,7 +255,8 @@ public Task> GetCompletionsAsync(string code, int curs } // Dynamic variable completions - foreach (var (name, desc) in DynamicVariables) + var dynamicVariables = DynamicVariables; + foreach (var (name, desc) in dynamicVariables) { var varPartial = ExtractPartialAfterDoubleBrace(beforeCursor); if (MatchesPrefix(name, varPartial)) @@ -269,8 +273,7 @@ public Task> GetCompletionsAsync(string code, int curs { if (MatchesPrefix(method, partial)) completions.Add(new Completion(method, method, "Keyword", - HttpMethodDescriptions.TryGetValue(method, out var d) ? d : null, - $"0_{method}")); + DescribeMethod(method), $"0_{method}")); } // Common headers @@ -278,8 +281,7 @@ public Task> GetCompletionsAsync(string code, int curs { if (MatchesPrefix(header, partial)) completions.Add(new Completion(header + ": ", header + ": ", "Property", - HeaderDescriptions.TryGetValue(header, out var d) ? d : null, - $"1_{header}")); + DescribeHeader(header), $"1_{header}")); } return Task.FromResult>(completions); @@ -296,7 +298,7 @@ public Task> GetDiagnosticsAsync(string code) { diagnostics.Add(new Diagnostic( DiagnosticSeverity.Error, - "No valid HTTP request found. Start with a method (GET, POST, etc.) followed by a URL.", + Strings.Diagnostic_NoRequest, 0, 0, 0, 0)); } @@ -305,7 +307,7 @@ public Task> GetDiagnosticsAsync(string code) if (string.IsNullOrWhiteSpace(request.Url)) { diagnostics.Add(new Diagnostic( - DiagnosticSeverity.Error, "Missing URL.", 0, 0, 0, 0)); + DiagnosticSeverity.Error, Strings.Diagnostic_MissingUrl, 0, 0, 0, 0)); } // Warn about unrecognized methods @@ -370,10 +372,10 @@ public Task> GetDiagnosticsAsync(string code) var (endLine, endCol) = OffsetToLineCol(code, wordEnd); var range = (startLine, startCol, endLine, endCol); - if (HttpMethodDescriptions.TryGetValue(word.ToUpperInvariant(), out var methodDesc)) + if (DescribeMethod(word) is { } methodDesc) return Task.FromResult(new HoverInfo(methodDesc, "text/plain", range)); - if (HeaderDescriptions.TryGetValue(word, out var headerDesc)) + if (DescribeHeader(word) is { } headerDesc) return Task.FromResult(new HoverInfo(headerDesc, "text/plain", range)); // Variable hover @@ -472,15 +474,21 @@ private static string TruncateValue(object? value, int maxLength = 100) "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS" }; - private static readonly Dictionary HttpMethodDescriptions = new(StringComparer.OrdinalIgnoreCase) + /// What a request method means, or null for a word that is not one. + /// + /// Looked up rather than held in a table, because a table built once would answer in + /// whichever language happened to be set when the kernel first loaded. + /// + private static string? DescribeMethod(string word) => word.ToUpperInvariant() switch { - ["GET"] = "GET — Retrieve a resource. Should not have side effects.", - ["POST"] = "POST — Submit data to create or process a resource.", - ["PUT"] = "PUT — Replace a resource entirely.", - ["PATCH"] = "PATCH — Partially update a resource.", - ["DELETE"] = "DELETE — Remove a resource.", - ["HEAD"] = "HEAD — Like GET but returns only headers, no body.", - ["OPTIONS"] = "OPTIONS — Describe communication options for the target resource.", + "GET" => Strings.Completion_Method_Get, + "POST" => Strings.Completion_Method_Post, + "PUT" => Strings.Completion_Method_Put, + "PATCH" => Strings.Completion_Method_Patch, + "DELETE" => Strings.Completion_Method_Delete, + "HEAD" => Strings.Completion_Method_Head, + "OPTIONS" => Strings.Completion_Method_Options, + _ => null, }; private static readonly string[] CommonHeaders = @@ -490,28 +498,32 @@ private static string TruncateValue(object? value, int maxLength = 100) "If-Modified-Since", "Accept-Encoding", "Accept-Language" }; - private static readonly Dictionary HeaderDescriptions = new(StringComparer.OrdinalIgnoreCase) + /// What a header is for, or null for a word that is not one. + private static string? DescribeHeader(string word) => word.ToLowerInvariant() switch { - ["Content-Type"] = "Content-Type — The media type of the request body (e.g., application/json).", - ["Authorization"] = "Authorization — Credentials for authenticating the request (e.g., Bearer token).", - ["Accept"] = "Accept — Media types the client can handle in the response.", - ["Cache-Control"] = "Cache-Control — Directives for caching mechanisms.", - ["User-Agent"] = "User-Agent — Identifies the client software making the request.", - ["Cookie"] = "Cookie — HTTP cookies previously sent by the server.", - ["X-Request-Id"] = "X-Request-Id — Custom header for request tracing.", - ["If-None-Match"] = "If-None-Match — Conditional request using ETag values.", - ["If-Modified-Since"] = "If-Modified-Since — Conditional request based on last modification date.", - ["Accept-Encoding"] = "Accept-Encoding — Acceptable content encoding (e.g., gzip, deflate).", - ["Accept-Language"] = "Accept-Language — Preferred natural languages for the response.", + "content-type" => Strings.Completion_Header_ContentType, + "authorization" => Strings.Completion_Header_Authorization, + "accept" => Strings.Completion_Header_Accept, + "cache-control" => Strings.Completion_Header_CacheControl, + "user-agent" => Strings.Completion_Header_UserAgent, + "cookie" => Strings.Completion_Header_Cookie, + "x-request-id" => Strings.Completion_Header_XRequestId, + "if-none-match" => Strings.Completion_Header_IfNoneMatch, + "if-modified-since" => Strings.Completion_Header_IfModifiedSince, + "accept-encoding" => Strings.Completion_Header_AcceptEncoding, + "accept-language" => Strings.Completion_Header_AcceptLanguage, + _ => null, }; - private static readonly (string Name, string Description)[] DynamicVariables = + /// The values a request can ask for by name, with what each one puts in. + /// Built on each call for the same reason the two lookups above are. + private static (string Name, string Description)[] DynamicVariables => new[] { - ("$guid", "Generate a new UUID/GUID."), - ("$randomInt", "Generate a random integer. Usage: $randomInt [min max]"), - ("$timestamp", "Current Unix timestamp in seconds. Usage: $timestamp [offset unit]"), - ("$datetime", "Current UTC datetime. Usage: $datetime [format] [offset unit]"), - ("$localDatetime", "Current local datetime. Usage: $localDatetime [format] [offset unit]"), - ("$processEnv", "Read an environment variable. Usage: $processEnv NAME"), + ("$guid", Strings.Completion_Variable_Guid), + ("$randomInt", Strings.Completion_Variable_RandomInt), + ("$timestamp", Strings.Completion_Variable_Timestamp), + ("$datetime", Strings.Completion_Variable_Datetime), + ("$localDatetime", Strings.Completion_Variable_LocalDatetime), + ("$processEnv", Strings.Completion_Variable_ProcessEnv), }; } diff --git a/src/Verso.Http/Localization/CellText.cs b/src/Verso.Http/Localization/CellText.cs new file mode 100644 index 00000000..aa057993 --- /dev/null +++ b/src/Verso.Http/Localization/CellText.cs @@ -0,0 +1,17 @@ +using Verso.Http.Resources; + +namespace Verso.Http.Localization; + +/// +/// Puts the standard prefix in front of a message written into a cell's output. +/// +/// +/// The prefix is one entry rather than the first word of several, so a translator sets it once +/// and every message that carries it reads the same. It is a placeholder rather than something +/// glued on the front, because a language may not put it there at all. +/// +internal static class CellText +{ + /// Marks a message about something a cell could not carry out. + public static string Error(string message) => string.Format(Strings.Prefix_Error, message); +} diff --git a/src/Verso.Http/MagicCommands/HttpSetBaseMagicCommand.cs b/src/Verso.Http/MagicCommands/HttpSetBaseMagicCommand.cs index 8b639e9d..097d94ce 100644 --- a/src/Verso.Http/MagicCommands/HttpSetBaseMagicCommand.cs +++ b/src/Verso.Http/MagicCommands/HttpSetBaseMagicCommand.cs @@ -1,5 +1,7 @@ using Verso.Abstractions; using Verso.Http.Kernel; +using Verso.Http.Localization; +using Verso.Http.Resources; namespace Verso.Http.MagicCommands; @@ -11,17 +13,17 @@ public sealed class HttpSetBaseMagicCommand : IMagicCommand { // --- IExtension --- public string ExtensionId => "verso.http.magic.http-set-base"; - string IExtension.Name => "HTTP Set Base Magic Command"; + string IExtension.Name => Strings.Magic_SetBase_Name; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string Description => "Sets the base URL for relative HTTP request URLs."; + public string Description => Strings.Magic_SetBase_Description; // --- IMagicCommand --- public string Name => "http-set-base"; public IReadOnlyList Parameters { get; } = new[] { - new ParameterDefinition("url", "The base URL to prepend to relative request URLs.", typeof(string), IsRequired: true), + new ParameterDefinition("url", Strings.Magic_SetBase_Param_Url, typeof(string), IsRequired: true), }; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; @@ -35,7 +37,7 @@ public async Task ExecuteAsync(string arguments, IMagicCommandContext context) if (string.IsNullOrWhiteSpace(url)) { await context.WriteOutputAsync(new CellOutput( - "text/plain", "Error: URL is required. Usage: #!http-set-base ", + "text/plain", CellText.Error(Strings.Magic_SetBase_UrlRequired), IsError: true)).ConfigureAwait(false); return; } @@ -43,6 +45,6 @@ await context.WriteOutputAsync(new CellOutput( context.Variables.Set(HttpKernel.BaseUrlStoreKey, url); await context.WriteOutputAsync(new CellOutput( - "text/plain", $"HTTP base URL set to: {url}")).ConfigureAwait(false); + "text/plain", string.Format(Strings.Magic_SetBase_Done, url))).ConfigureAwait(false); } } diff --git a/src/Verso.Http/MagicCommands/HttpSetHeaderMagicCommand.cs b/src/Verso.Http/MagicCommands/HttpSetHeaderMagicCommand.cs index 75196c12..695cf902 100644 --- a/src/Verso.Http/MagicCommands/HttpSetHeaderMagicCommand.cs +++ b/src/Verso.Http/MagicCommands/HttpSetHeaderMagicCommand.cs @@ -1,5 +1,7 @@ using Verso.Abstractions; using Verso.Http.Kernel; +using Verso.Http.Localization; +using Verso.Http.Resources; namespace Verso.Http.MagicCommands; @@ -11,18 +13,18 @@ public sealed class HttpSetHeaderMagicCommand : IMagicCommand { // --- IExtension --- public string ExtensionId => "verso.http.magic.http-set-header"; - string IExtension.Name => "HTTP Set Header Magic Command"; + string IExtension.Name => Strings.Magic_SetHeader_Name; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string Description => "Adds or updates a default HTTP header for all requests."; + public string Description => Strings.Magic_SetHeader_Description; // --- IMagicCommand --- public string Name => "http-set-header"; public IReadOnlyList Parameters { get; } = new[] { - new ParameterDefinition("name", "The header name.", typeof(string), IsRequired: true), - new ParameterDefinition("value", "The header value.", typeof(string), IsRequired: true), + new ParameterDefinition("name", Strings.Magic_SetHeader_Param_Name, typeof(string), IsRequired: true), + new ParameterDefinition("value", Strings.Magic_SetHeader_Param_Value, typeof(string), IsRequired: true), }; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; @@ -37,7 +39,7 @@ public async Task ExecuteAsync(string arguments, IMagicCommandContext context) if (spaceIndex < 0) { await context.WriteOutputAsync(new CellOutput( - "text/plain", "Error: Both header name and value are required. Usage: #!http-set-header ", + "text/plain", CellText.Error(Strings.Magic_SetHeader_BothRequired), IsError: true)).ConfigureAwait(false); return; } @@ -48,7 +50,7 @@ await context.WriteOutputAsync(new CellOutput( if (string.IsNullOrWhiteSpace(headerValue)) { await context.WriteOutputAsync(new CellOutput( - "text/plain", "Error: Header value cannot be empty. Usage: #!http-set-header ", + "text/plain", CellText.Error(Strings.Magic_SetHeader_ValueRequired), IsError: true)).ConfigureAwait(false); return; } @@ -60,6 +62,6 @@ await context.WriteOutputAsync(new CellOutput( context.Variables.Set(HttpKernel.DefaultHeadersStoreKey, headers); await context.WriteOutputAsync(new CellOutput( - "text/plain", $"Default header set: {headerName}: {headerValue}")).ConfigureAwait(false); + "text/plain", string.Format(Strings.Magic_SetHeader_Done, headerName, headerValue))).ConfigureAwait(false); } } diff --git a/src/Verso.Http/MagicCommands/HttpSetTimeoutMagicCommand.cs b/src/Verso.Http/MagicCommands/HttpSetTimeoutMagicCommand.cs index 5caa1c3b..3c071c07 100644 --- a/src/Verso.Http/MagicCommands/HttpSetTimeoutMagicCommand.cs +++ b/src/Verso.Http/MagicCommands/HttpSetTimeoutMagicCommand.cs @@ -1,5 +1,7 @@ using Verso.Abstractions; using Verso.Http.Kernel; +using Verso.Http.Localization; +using Verso.Http.Resources; namespace Verso.Http.MagicCommands; @@ -11,17 +13,17 @@ public sealed class HttpSetTimeoutMagicCommand : IMagicCommand { // --- IExtension --- public string ExtensionId => "verso.http.magic.http-set-timeout"; - string IExtension.Name => "HTTP Set Timeout Magic Command"; + string IExtension.Name => Strings.Magic_SetTimeout_Name; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string Description => "Sets the default timeout (in seconds) for HTTP requests."; + public string Description => Strings.Magic_SetTimeout_Description; // --- IMagicCommand --- public string Name => "http-set-timeout"; public IReadOnlyList Parameters { get; } = new[] { - new ParameterDefinition("seconds", "The timeout in seconds.", typeof(int), IsRequired: true), + new ParameterDefinition("seconds", Strings.Magic_SetTimeout_Param_Seconds, typeof(int), IsRequired: true), }; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; @@ -35,7 +37,7 @@ public async Task ExecuteAsync(string arguments, IMagicCommandContext context) if (!int.TryParse(trimmed, out var seconds) || seconds <= 0) { await context.WriteOutputAsync(new CellOutput( - "text/plain", "Error: A positive integer is required. Usage: #!http-set-timeout ", + "text/plain", CellText.Error(Strings.Magic_SetTimeout_Invalid), IsError: true)).ConfigureAwait(false); return; } @@ -43,6 +45,6 @@ await context.WriteOutputAsync(new CellOutput( context.Variables.Set(HttpKernel.TimeoutStoreKey, seconds); await context.WriteOutputAsync(new CellOutput( - "text/plain", $"HTTP timeout set to {seconds} seconds.")).ConfigureAwait(false); + "text/plain", string.Format(Strings.Magic_SetTimeout_Done, seconds))).ConfigureAwait(false); } } diff --git a/src/Verso.Http/Resources/Strings.de.resx b/src/Verso.Http/Resources/Strings.de.resx new file mode 100644 index 00000000..1dbb62c8 --- /dev/null +++ b/src/Verso.Http/Resources/Strings.de.resx @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + HTTP-Anfragezellentyp zum Senden von REST-API-Anfragen. + + + HTTP-Zellentyp + + + Accept — Medientypen, die der Client in der Antwort verarbeiten kann. + + + Accept-Encoding — Zulässige Inhaltskodierung (z. B. gzip, deflate). + + + Accept-Language — Bevorzugte natürliche Sprachen für die Antwort. + + + Authorization — Anmeldeinformationen zur Authentifizierung der Anfrage (z. B. Bearer-Token). + + + Cache-Control — Direktiven für Caching-Mechanismen. + + + Content-Type — Der Medientyp des Anfragetexts (z. B. application/json). + + + Cookie — HTTP-Cookies, die der Server zuvor gesendet hat. + + + If-Modified-Since — Bedingte Anfrage anhand des letzten Änderungsdatums. + + + If-None-Match — Bedingte Anfrage anhand von ETag-Werten. + + + User-Agent — Kennzeichnet die Clientsoftware, die die Anfrage stellt. + + + X-Request-Id — Benutzerdefinierter Header zur Nachverfolgung von Anfragen. + + + DELETE — Entfernt eine Ressource. + + + GET — Ruft eine Ressource ab. Sollte keine Nebenwirkungen haben. + + + HEAD — Wie GET, gibt aber nur Header zurück, keinen Text. + + + OPTIONS — Beschreibt die Kommunikationsoptionen der Zielressource. + + + PATCH — Aktualisiert eine Ressource teilweise. + + + POST — Sendet Daten, um eine Ressource zu erstellen oder zu verarbeiten. + + + PUT — Ersetzt eine Ressource vollständig. + + + Aktuelles UTC-Datum mit Uhrzeit. Verwendung: $datetime [format] [offset unit] + + + Erzeugt eine neue UUID/GUID. + + + Aktuelles lokales Datum mit Uhrzeit. Verwendung: $localDatetime [format] [offset unit] + + + Liest eine Umgebungsvariable. Verwendung: $processEnv NAME + + + Erzeugt eine zufällige ganze Zahl. Verwendung: $randomInt [min max] + + + Aktueller Unix-Zeitstempel in Sekunden. Verwendung: $timestamp [offset unit] + + + Fehlende URL. + + + Keine gültige HTTP-Anfrage gefunden. Beginnen Sie mit einer Methode (GET, POST usw.), gefolgt von einer URL. + + + Führt HTTP-Anfragen in der Syntax von .http-Dateien aus. + + + HTTP-Kernel + + + Legt die Basis-URL für relative URLs in HTTP-Anfragen fest. + + + HTTP-Basis-URL festgelegt auf: {0} + + + HTTP-Set-Base-Magic-Command + + + Die Basis-URL, die relativen Anfrage-URLs vorangestellt wird. + + + Eine URL ist erforderlich. Verwendung: #!http-set-base <url> + + + Headername und Wert sind beide erforderlich. Verwendung: #!http-set-header <name> <value> + + + Fügt einen HTTP-Standardheader für alle Anfragen hinzu oder ändert ihn. + + + Standardheader festgelegt: {0}: {1} + + + HTTP-Set-Header-Magic-Command + + + Der Headername. + + + Der Headerwert. + + + Der Headerwert darf nicht leer sein. Verwendung: #!http-set-header <name> <value> + + + Legt das Standardzeitlimit (in Sekunden) für HTTP-Anfragen fest. + + + HTTP-Zeitlimit auf {0} Sekunden festgelegt. + + + Eine positive ganze Zahl ist erforderlich. Verwendung: #!http-set-timeout <seconds> + + + HTTP-Set-Timeout-Magic-Command + + + Das Zeitlimit in Sekunden. + + + Fehler: {0} + + + Stellt HTTP-Anfragezellen dar. + + + HTTP-Renderer + + + Antwortheader ({0}) + + + Die Antwort wurde bei 100 KB abgeschnitten (insgesamt: {0} KB) + + + Die Anfrage wurde abgebrochen. + + + Keine HTTP-Anfrage gefunden. + + + Die relative URL '{0}' benötigt eine Basis-URL. Verwenden Sie #!http-set-base. + + + Die Anfrage ist mit dem Status {0} {1} fehlgeschlagen. + + + {0} {1} wird gesendet... + + + Zeitüberschreitung der Anfrage nach {0}s. + + \ No newline at end of file diff --git a/src/Verso.Http/Resources/Strings.es.resx b/src/Verso.Http/Resources/Strings.es.resx new file mode 100644 index 00000000..7113fb99 --- /dev/null +++ b/src/Verso.Http/Resources/Strings.es.resx @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Tipo de celda de solicitud HTTP para enviar peticiones a API REST. + + + Tipo de celda HTTP + + + Accept — Tipos de medio que el cliente puede procesar en la respuesta. + + + Accept-Encoding — Codificaciones de contenido aceptables (por ejemplo, gzip, deflate). + + + Accept-Language — Idiomas naturales preferidos para la respuesta. + + + Authorization — Credenciales para autenticar la solicitud (por ejemplo, un token Bearer). + + + Cache-Control — Directivas para los mecanismos de caché. + + + Content-Type — El tipo de medio del cuerpo de la solicitud (por ejemplo, application/json). + + + Cookie — Cookies HTTP enviadas anteriormente por el servidor. + + + If-Modified-Since — Solicitud condicional según la fecha de la última modificación. + + + If-None-Match — Solicitud condicional que usa valores ETag. + + + User-Agent — Identifica el software cliente que hace la solicitud. + + + X-Request-Id — Encabezado personalizado para el seguimiento de solicitudes. + + + DELETE — Quita un recurso. + + + GET — Recupera un recurso. No debería tener efectos secundarios. + + + HEAD — Como GET, pero devuelve solo los encabezados, sin cuerpo. + + + OPTIONS — Describe las opciones de comunicación del recurso de destino. + + + PATCH — Actualiza parcialmente un recurso. + + + POST — Envía datos para crear o procesar un recurso. + + + PUT — Reemplaza un recurso por completo. + + + Fecha y hora UTC actuales. Uso: $datetime [format] [offset unit] + + + Genera un UUID/GUID nuevo. + + + Fecha y hora locales actuales. Uso: $localDatetime [format] [offset unit] + + + Lee una variable de entorno. Uso: $processEnv NAME + + + Genera un número entero aleatorio. Uso: $randomInt [min max] + + + Marca de tiempo Unix actual en segundos. Uso: $timestamp [offset unit] + + + Falta la URL. + + + No se encontró ninguna solicitud HTTP válida. Empiece con un método (GET, POST, etc.) seguido de una URL. + + + Ejecuta solicitudes HTTP con la sintaxis de los archivos .http. + + + Kernel de HTTP + + + Establece la URL base de las URL relativas de las solicitudes HTTP. + + + URL base de HTTP establecida en: {0} + + + Comando mágico HTTP Set Base + + + La URL base que se antepone a las URL de solicitud relativas. + + + La URL es obligatoria. Uso: #!http-set-base <url> + + + El nombre y el valor del encabezado son obligatorios. Uso: #!http-set-header <name> <value> + + + Añade o actualiza un encabezado HTTP predeterminado para todas las solicitudes. + + + Encabezado predeterminado establecido: {0}: {1} + + + Comando mágico HTTP Set Header + + + El nombre del encabezado. + + + El valor del encabezado. + + + El valor del encabezado no puede estar vacío. Uso: #!http-set-header <name> <value> + + + Establece el tiempo de espera predeterminado (en segundos) de las solicitudes HTTP. + + + Tiempo de espera de HTTP establecido en {0} segundos. + + + Se necesita un número entero positivo. Uso: #!http-set-timeout <seconds> + + + Comando mágico HTTP Set Timeout + + + El tiempo de espera en segundos. + + + Error: {0} + + + Renderiza las celdas de solicitud HTTP. + + + Renderizador de HTTP + + + Encabezados de la respuesta ({0}) + + + Respuesta recortada en 100 KB (total: {0} KB) + + + Solicitud cancelada. + + + No se encontró ninguna solicitud HTTP. + + + La URL relativa '{0}' necesita una URL base. Use #!http-set-base. + + + La solicitud falló con el estado {0} {1}. + + + Enviando {0} {1}... + + + La solicitud agotó el tiempo de espera tras {0}s. + + \ No newline at end of file diff --git a/src/Verso.Http/Resources/Strings.ja.resx b/src/Verso.Http/Resources/Strings.ja.resx new file mode 100644 index 00000000..12b80332 --- /dev/null +++ b/src/Verso.Http/Resources/Strings.ja.resx @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + REST API へのリクエストを送るための HTTP リクエストセルタイプ。 + + + HTTP セルタイプ + + + Accept — クライアントが応答として扱えるメディアタイプ。 + + + Accept-Encoding — 受け入れ可能なコンテンツエンコーディング (gzip、deflate など)。 + + + Accept-Language — 応答に希望する自然言語。 + + + Authorization — リクエストを認証するための資格情報 (Bearer トークンなど)。 + + + Cache-Control — キャッシュの動作を指定するディレクティブ。 + + + Content-Type — リクエスト本文のメディアタイプ (application/json など)。 + + + Cookie — サーバーが以前に送った HTTP クッキー。 + + + If-Modified-Since — 最終更新日時に基づく条件付きリクエスト。 + + + If-None-Match — ETag の値を使う条件付きリクエスト。 + + + User-Agent — リクエストを送るクライアントソフトウェアを示します。 + + + X-Request-Id — リクエストの追跡に使うカスタムヘッダー。 + + + DELETE — リソースを削除します。 + + + GET — リソースを取得します。副作用があってはいけません。 + + + HEAD — GET と同じですが、本文を返さずヘッダーだけを返します。 + + + OPTIONS — 対象リソースに対する通信オプションを示します。 + + + PATCH — リソースの一部を更新します。 + + + POST — リソースを作成または処理するためにデータを送信します。 + + + PUT — リソース全体を置き換えます。 + + + 現在の UTC 日時。使い方: $datetime [format] [offset unit] + + + 新しい UUID/GUID を生成します。 + + + 現在のローカル日時。使い方: $localDatetime [format] [offset unit] + + + 環境変数を読み取ります。使い方: $processEnv NAME + + + ランダムな整数を生成します。使い方: $randomInt [min max] + + + 現在の Unix タイムスタンプ (秒)。使い方: $timestamp [offset unit] + + + URL がありません。 + + + 有効な HTTP リクエストが見つかりません。メソッド (GET、POST など) に続けて URL を書いてください。 + + + .http ファイルの記法で HTTP リクエストを実行します。 + + + HTTP カーネル + + + 相対的な HTTP リクエスト URL の基準となる URL を設定します。 + + + HTTP の基準 URL を設定しました: {0} + + + HTTP Set Base マジックコマンド + + + 相対的なリクエスト URL の前に付ける基準 URL。 + + + URL は必須です。使い方: #!http-set-base <url> + + + ヘッダーの名前と値の両方が必要です。使い方: #!http-set-header <name> <value> + + + すべてのリクエストに付ける既定の HTTP ヘッダーを追加または更新します。 + + + 既定のヘッダーを設定しました: {0}: {1} + + + HTTP Set Header マジックコマンド + + + ヘッダーの名前。 + + + ヘッダーの値。 + + + ヘッダーの値は空にできません。使い方: #!http-set-header <name> <value> + + + HTTP リクエストの既定のタイムアウト (秒) を設定します。 + + + HTTP のタイムアウトを {0} 秒に設定しました。 + + + 正の整数が必要です。使い方: #!http-set-timeout <seconds> + + + HTTP Set Timeout マジックコマンド + + + タイムアウトの秒数。 + + + エラー: {0} + + + HTTP リクエストのセルを表示します。 + + + HTTP レンダラー + + + 応答ヘッダー ({0}) + + + 応答を 100 KB で打ち切りました (全体: {0} KB) + + + リクエストを中止しました。 + + + HTTP リクエストが見つかりません。 + + + 相対 URL '{0}' には基準 URL が必要です。#!http-set-base を使ってください。 + + + リクエストがステータス {0} {1} で失敗しました。 + + + {0} {1} を送信しています... + + + {0}s でリクエストがタイムアウトしました。 + + \ No newline at end of file diff --git a/src/Verso.Http/Resources/Strings.qps-Ploc.resx b/src/Verso.Http/Resources/Strings.qps-Ploc.resx new file mode 100644 index 00000000..c448b592 --- /dev/null +++ b/src/Verso.Http/Resources/Strings.qps-Ploc.resx @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + [!!HTTP réqùéšt çéll tÿpé fòr šéñdïñg RÉŠT ÀPÏ réqùéštš.···!!] + + + [!!HTTP Çéll Tÿpé···!!] + + + [!!Àççépt — Médïà tÿpéš thé çlïéñt çàñ hàñdlé ïñ thé réšpòñšé.···!!] + + + [!!Àççépt-Éñçòdïñg — Àççéptàblé çòñtéñt éñçòdïñg (é.g., gzïp, déflàté).···!!] + + + [!!Àççépt-Làñgùàgé — Préférréd ñàtùràl làñgùàgéš fòr thé réšpòñšé.···!!] + + + [!!Àùthòrïzàtïòñ — Çrédéñtïàlš fòr àùthéñtïçàtïñg thé réqùéšt (é.g., Béàrér tòkéñ).···!!] + + + [!!Çàçhé-Çòñtròl — Dïréçtïvéš fòr çàçhïñg méçhàñïšmš.···!!] + + + [!!Çòñtéñt-Tÿpé — Thé médïà tÿpé òf thé réqùéšt bòdÿ (é.g., àpplïçàtïòñ/jšòñ).···!!] + + + [!!Çòòkïé — HTTP çòòkïéš prévïòùšlÿ šéñt bÿ thé šérvér.···!!] + + + [!!Ïf-Mòdïfïéd-Šïñçé — Çòñdïtïòñàl réqùéšt bàšéd òñ làšt mòdïfïçàtïòñ dàté.···!!] + + + [!!Ïf-Ñòñé-Màtçh — Çòñdïtïòñàl réqùéšt ùšïñg ÉTàg vàlùéš.···!!] + + + [!!Ùšér-Àgéñt — Ïdéñtïfïéš thé çlïéñt šòftwàré màkïñg thé réqùéšt.···!!] + + + [!!X-Réqùéšt-Ïd — Çùštòm héàdér fòr réqùéšt tràçïñg.···!!] + + + [!!DÉLÉTÉ — Rémòvé à réšòùrçé.···!!] + + + [!!GÉT — Rétrïévé à réšòùrçé. Šhòùld ñòt hàvé šïdé éfféçtš.···!!] + + + [!!HÉÀD — Lïké GÉT bùt rétùrñš òñlÿ héàdérš, ñò bòdÿ.···!!] + + + [!!ÒPTÏÒÑŠ — Déšçrïbé çòmmùñïçàtïòñ òptïòñš fòr thé tàrgét réšòùrçé.···!!] + + + [!!PÀTÇH — Pàrtïàllÿ ùpdàté à réšòùrçé.···!!] + + + [!!PÒŠT — Šùbmït dàtà tò çréàté òr pròçéšš à réšòùrçé.···!!] + + + [!!PÙT — Réplàçé à réšòùrçé éñtïrélÿ.···!!] + + + [!!Çùrréñt ÙTÇ dàtétïmé. Ùšàgé: $dàtétïmé [fòrmàt] [òffšét ùñït]···!!] + + + [!!Géñéràté à ñéw ÙÙÏD/GÙÏD.···!!] + + + [!!Çùrréñt lòçàl dàtétïmé. Ùšàgé: $lòçàlDàtétïmé [fòrmàt] [òffšét ùñït]···!!] + + + [!!Réàd àñ éñvïròñméñt vàrïàblé. Ùšàgé: $pròçéššÉñv ÑÀMÉ···!!] + + + [!!Géñéràté à ràñdòm ïñtégér. Ùšàgé: $ràñdòmÏñt [mïñ màx]···!!] + + + [!!Çùrréñt Ùñïx tïméštàmp ïñ šéçòñdš. Ùšàgé: $tïméštàmp [òffšét ùñït]···!!] + + + [!!Mïššïñg ÙRL.···!!] + + + [!!Ñò vàlïd HTTP réqùéšt fòùñd. Štàrt wïth à méthòd (GÉT, PÒŠT, étç.) fòllòwéd bÿ à ÙRL.···!!] + + + [!!Éxéçùtéš HTTP réqùéštš ùšïñg .http fïlé šÿñtàx.···!!] + + + [!!HTTP Kérñél···!!] + + + [!!Šétš thé bàšé ÙRL fòr rélàtïvé HTTP réqùéšt ÙRLš.···!!] + + + [!!HTTP bàšé ÙRL šét tò: {0}···!!] + + + [!!HTTP Šét Bàšé Màgïç Çòmmàñd···!!] + + + [!!Thé bàšé ÙRL tò prépéñd tò rélàtïvé réqùéšt ÙRLš.···!!] + + + [!!ÙRL ïš réqùïréd. Ùšàgé: #!http-šét-bàšé <ùrl>···!!] + + + [!!Bòth héàdér ñàmé àñd vàlùé àré réqùïréd. Ùšàgé: #!http-šét-héàdér <ñàmé> <vàlùé>···!!] + + + [!!Àddš òr ùpdàtéš à défàùlt HTTP héàdér fòr àll réqùéštš.···!!] + + + [!!Défàùlt héàdér šét: {0}: {1}···!!] + + + [!!HTTP Šét Héàdér Màgïç Çòmmàñd···!!] + + + [!!Thé héàdér ñàmé.···!!] + + + [!!Thé héàdér vàlùé.···!!] + + + [!!Héàdér vàlùé çàññòt bé émptÿ. Ùšàgé: #!http-šét-héàdér <ñàmé> <vàlùé>···!!] + + + [!!Šétš thé défàùlt tïméòùt (ïñ šéçòñdš) fòr HTTP réqùéštš.···!!] + + + [!!HTTP tïméòùt šét tò {0} šéçòñdš.···!!] + + + [!!À pòšïtïvé ïñtégér ïš réqùïréd. Ùšàgé: #!http-šét-tïméòùt <šéçòñdš>···!!] + + + [!!HTTP Šét Tïméòùt Màgïç Çòmmàñd···!!] + + + [!!Thé tïméòùt ïñ šéçòñdš.···!!] + + + [!!Érròr: {0}···!!] + + + [!!Réñdérš HTTP réqùéšt çéllš.···!!] + + + [!!HTTP Réñdérér···!!] + + + [!!Réšpòñšé Héàdérš ({0})···!!] + + + [!!Réšpòñšé trùñçàtéd àt 100 KB (tòtàl: {0} KB)···!!] + + + [!!Réqùéšt çàñçélléd.···!!] + + + [!!Ñò HTTP réqùéšt fòùñd.···!!] + + + [!!Rélàtïvé ÙRL '{0}' réqùïréš à bàšé ÙRL. Ùšé #!http-šét-bàšé.···!!] + + + [!!Réqùéšt fàïléd wïth štàtùš {0} {1}.···!!] + + + [!!Šéñdïñg {0} {1}...···!!] + + + [!!Réqùéšt tïméd òùt àftér {0}š.···!!] + + \ No newline at end of file diff --git a/src/Verso.Http/Resources/Strings.resx b/src/Verso.Http/Resources/Strings.resx new file mode 100644 index 00000000..4e7b95a9 --- /dev/null +++ b/src/Verso.Http/Resources/Strings.resx @@ -0,0 +1,293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + HTTP request cell type for sending REST API requests. + What an HTTP cell is, shown when choosing a cell type. + + + HTTP Cell Type + Name of the HTTP cell type, as listed in the Extensions panel. + + + Accept — Media types the client can handle in the response. + What the Accept header is for, offered while typing and shown on hover. The header name at the start is part of the standard and stays as written. + + + Accept-Encoding — Acceptable content encoding (e.g., gzip, deflate). + What the Accept-Encoding header is for, offered while typing and shown on hover. The header name at the start is part of the standard and stays as written. + + + Accept-Language — Preferred natural languages for the response. + What the Accept-Language header is for, offered while typing and shown on hover. The header name at the start is part of the standard and stays as written. + + + Authorization — Credentials for authenticating the request (e.g., Bearer token). + What the Authorization header is for, offered while typing and shown on hover. The header name at the start is part of the standard and stays as written. + + + Cache-Control — Directives for caching mechanisms. + What the Cache-Control header is for, offered while typing and shown on hover. The header name at the start is part of the standard and stays as written. + + + Content-Type — The media type of the request body (e.g., application/json). + What the Content-Type header is for, offered while typing and shown on hover. The header name at the start is part of the standard and stays as written. + + + Cookie — HTTP cookies previously sent by the server. + What the Cookie header is for, offered while typing and shown on hover. The header name at the start is part of the standard and stays as written. + + + If-Modified-Since — Conditional request based on last modification date. + What the If-Modified-Since header is for, offered while typing and shown on hover. The header name at the start is part of the standard and stays as written. + + + If-None-Match — Conditional request using ETag values. + What the If-None-Match header is for, offered while typing and shown on hover. The header name at the start is part of the standard and stays as written. + + + User-Agent — Identifies the client software making the request. + What the User-Agent header is for, offered while typing and shown on hover. The header name at the start is part of the standard and stays as written. + + + X-Request-Id — Custom header for request tracing. + What the X-Request-Id header is for, offered while typing and shown on hover. The header name at the start is part of the standard and stays as written. + + + DELETE — Remove a resource. + What DELETE means, offered while typing and shown on hover. The method name at the start is part of the standard and stays as written. + + + GET — Retrieve a resource. Should not have side effects. + What GET means, offered while typing and shown on hover. The method name at the start is part of the standard and stays as written. + + + HEAD — Like GET but returns only headers, no body. + What HEAD means, offered while typing and shown on hover. The method name at the start is part of the standard and stays as written. + + + OPTIONS — Describe communication options for the target resource. + What OPTIONS means, offered while typing and shown on hover. The method name at the start is part of the standard and stays as written. + + + PATCH — Partially update a resource. + What PATCH means, offered while typing and shown on hover. The method name at the start is part of the standard and stays as written. + + + POST — Submit data to create or process a resource. + What POST means, offered while typing and shown on hover. The method name at the start is part of the standard and stays as written. + + + PUT — Replace a resource entirely. + What PUT means, offered while typing and shown on hover. The method name at the start is part of the standard and stays as written. + + + Current UTC datetime. Usage: $datetime [format] [offset unit] + What $datetime puts in a request, offered while typing. Anything after 'Usage:' is typed at a keyboard and stays as written. + + + Generate a new UUID/GUID. + What $guid puts in a request, offered while typing. Anything after 'Usage:' is typed at a keyboard and stays as written. + + + Current local datetime. Usage: $localDatetime [format] [offset unit] + What $localDatetime puts in a request, offered while typing. Anything after 'Usage:' is typed at a keyboard and stays as written. + + + Read an environment variable. Usage: $processEnv NAME + What $processEnv puts in a request, offered while typing. Anything after 'Usage:' is typed at a keyboard and stays as written. + + + Generate a random integer. Usage: $randomInt [min max] + What $randomInt puts in a request, offered while typing. Anything after 'Usage:' is typed at a keyboard and stays as written. + + + Current Unix timestamp in seconds. Usage: $timestamp [offset unit] + What $timestamp puts in a request, offered while typing. Anything after 'Usage:' is typed at a keyboard and stays as written. + + + Missing URL. + Marked against a request line with a method and nothing after it. + + + No valid HTTP request found. Start with a method (GET, POST, etc.) followed by a URL. + Marked against a cell that holds text but no request. The method names stay as written. + + + Executes HTTP requests using .http file syntax. + What the HTTP kernel is, shown wherever kernels are listed. .http is a file extension and stays as written. + + + HTTP Kernel + Name of the HTTP kernel, as listed in the Extensions panel. + + + Sets the base URL for relative HTTP request URLs. + What #!http-set-base does, shown in the list of magic commands. + + + HTTP base URL set to: {0} + {0} is the address that was set. + + + HTTP Set Base Magic Command + Name of the #!http-set-base magic command as an extension, as listed in the Extensions panel. The command word itself is typed into a cell and is not translated. + + + The base URL to prepend to relative request URLs. + What the argument to #!http-set-base is. + + + URL is required. Usage: #!http-set-base <url> + Shown when #!http-set-base is given nothing. Everything after 'Usage:' is typed at a keyboard and stays as written. + + + Both header name and value are required. Usage: #!http-set-header <name> <value> + Shown when #!http-set-header is given too little. Everything after 'Usage:' is typed at a keyboard and stays as written. + + + Adds or updates a default HTTP header for all requests. + What #!http-set-header does, shown in the list of magic commands. + + + Default header set: {0}: {1} + {0} is the header name and {1} its value, both as typed. + + + HTTP Set Header Magic Command + Name of the #!http-set-header magic command as an extension, as listed in the Extensions panel. The command word itself is typed into a cell and is not translated. + + + The header name. + What the first argument to #!http-set-header is. + + + The header value. + What the second argument to #!http-set-header is. + + + Header value cannot be empty. Usage: #!http-set-header <name> <value> + Shown when the header was named but given no value. + + + Sets the default timeout (in seconds) for HTTP requests. + What #!http-set-timeout does, shown in the list of magic commands. + + + HTTP timeout set to {0} seconds. + {0} is the number of seconds that was set. + + + A positive integer is required. Usage: #!http-set-timeout <seconds> + Shown when #!http-set-timeout is given something that is not a whole number above zero. + + + HTTP Set Timeout Magic Command + Name of the #!http-set-timeout magic command as an extension, as listed in the Extensions panel. The command word itself is typed into a cell and is not translated. + + + The timeout in seconds. + What the argument to #!http-set-timeout is. + + + Error: {0} + Written in front of a message a cell could not carry out. {0} is the message. + + + Renders HTTP request cells. + What this renderer does, shown wherever extensions are listed. + + + HTTP Renderer + Name of the HTTP response renderer, as listed in the Extensions panel. + + + Response Headers ({0}) + A heading the reader can open to see what the server sent back. {0} is how many there are. + + + Response truncated at 100 KB (total: {0} KB) + Shown under a body too long to show whole. {0} is the full size in kilobytes. + + + Request cancelled. + The reader stopped the cell before the answer came back. + + + No HTTP request found. + The cell holds nothing that reads as a request. + + + Relative URL '{0}' requires a base URL. Use #!http-set-base. + The cell asks for a path with no address to hang it on. {0} is the path as written. + + + Request failed with status {0} {1}. + The server answered, and said no. {0} is the status number and {1} the reason the server gave, which arrives in whatever language the server chose. + + + Sending {0} {1}... + Printed before the request goes out. {0} is the method, such as GET, and {1} the address; both stay as written. + + + Request timed out after {0}s. + No answer came back in time. {0} is the number of seconds waited; the 's' after it is the unit and stays as written. + + \ No newline at end of file diff --git a/src/Verso.Http/Resources/Strings.zh-Hans.resx b/src/Verso.Http/Resources/Strings.zh-Hans.resx new file mode 100644 index 00000000..4dd99afe --- /dev/null +++ b/src/Verso.Http/Resources/Strings.zh-Hans.resx @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 用于发送 REST API 请求的 HTTP 请求单元格类型。 + + + HTTP 单元格类型 + + + Accept — 客户端在响应中可以处理的媒体类型。 + + + Accept-Encoding — 可接受的内容编码(例如 gzip、deflate)。 + + + Accept-Language — 响应首选的自然语言。 + + + Authorization — 用于验证请求的凭据(例如 Bearer 令牌)。 + + + Cache-Control — 缓存机制的指令。 + + + Content-Type — 请求正文的媒体类型(例如 application/json)。 + + + Cookie — 服务器先前发送的 HTTP cookie。 + + + If-Modified-Since — 基于上次修改日期的条件请求。 + + + If-None-Match — 使用 ETag 值的条件请求。 + + + User-Agent — 标识发出请求的客户端软件。 + + + X-Request-Id — 用于请求跟踪的自定义标头。 + + + DELETE — 删除资源。 + + + GET — 检索资源。不应有副作用。 + + + HEAD — 与 GET 类似,但只返回标头,不返回正文。 + + + OPTIONS — 描述目标资源的通信选项。 + + + PATCH — 部分更新资源。 + + + POST — 提交数据以创建或处理资源。 + + + PUT — 完全替换资源。 + + + 当前的 UTC 日期时间。用法:$datetime [format] [offset unit] + + + 生成新的 UUID/GUID。 + + + 当前的本地日期时间。用法:$localDatetime [format] [offset unit] + + + 读取环境变量。用法:$processEnv NAME + + + 生成随机整数。用法:$randomInt [min max] + + + 当前的 Unix 时间戳,以秒为单位。用法:$timestamp [offset unit] + + + 缺少 URL。 + + + 找不到有效的 HTTP 请求。请以方法(GET、POST 等)开头,后跟 URL。 + + + 使用 .http 文件语法执行 HTTP 请求。 + + + HTTP 内核 + + + 为相对的 HTTP 请求 URL 设置基址。 + + + HTTP 基址已设置为:{0} + + + HTTP Set Base 魔法命令 + + + 要添加到相对请求 URL 前面的基址。 + + + 必须提供 URL。用法:#!http-set-base <url> + + + 标头名称和值都必须提供。用法:#!http-set-header <name> <value> + + + 为所有请求添加或更新默认的 HTTP 标头。 + + + 已设置默认标头:{0}: {1} + + + HTTP Set Header 魔法命令 + + + 标头名称。 + + + 标头值。 + + + 标头值不能为空。用法:#!http-set-header <name> <value> + + + 设置 HTTP 请求的默认超时(以秒为单位)。 + + + HTTP 超时已设置为 {0} 秒。 + + + 必须提供正整数。用法:#!http-set-timeout <seconds> + + + HTTP Set Timeout 魔法命令 + + + 超时秒数。 + + + 错误:{0} + + + 渲染 HTTP 请求单元格。 + + + HTTP 渲染器 + + + 响应标头({0}) + + + 响应已在 100 KB 处截断(总计:{0} KB) + + + 请求已取消。 + + + 找不到 HTTP 请求。 + + + 相对 URL“{0}”需要基址。请使用 #!http-set-base。 + + + 请求失败,状态为 {0} {1}。 + + + 正在发送 {0} {1}... + + + 请求在 {0}s 后超时。 + + \ No newline at end of file diff --git a/src/Verso.Http/Verso.Http.csproj b/src/Verso.Http/Verso.Http.csproj index 203a6968..fc742043 100644 --- a/src/Verso.Http/Verso.Http.csproj +++ b/src/Verso.Http/Verso.Http.csproj @@ -15,6 +15,23 @@ + + + + MSBuild:Compile + $(IntermediateOutputPath)Strings.Designer.cs + CSharp + Verso.Http.Resources + Strings + + + diff --git a/src/Verso.JavaScript/Kernel/IJavaScriptRunner.cs b/src/Verso.JavaScript/Kernel/IJavaScriptRunner.cs index 42d4c2b6..334a719c 100644 --- a/src/Verso.JavaScript/Kernel/IJavaScriptRunner.cs +++ b/src/Verso.JavaScript/Kernel/IJavaScriptRunner.cs @@ -1,3 +1,4 @@ +using Verso.JavaScript.Resources; namespace Verso.JavaScript.Kernel; /// @@ -30,7 +31,7 @@ internal interface IJavaScriptRunner : IAsyncDisposable /// Only supported by Node.js runner when the typescript module is installed. /// Task TranspileAsync(string code, CancellationToken ct) => - Task.FromResult(new TranspileResult(null, "TypeScript transpilation requires Node.js with the typescript module installed.")); + Task.FromResult(new TranspileResult(null, Strings.Node_TranspileRequiresModule)); /// /// True if the backend is still operational. For Node.js, false after a process crash. diff --git a/src/Verso.JavaScript/Kernel/JavaScriptKernel.cs b/src/Verso.JavaScript/Kernel/JavaScriptKernel.cs index e9766361..96ea691f 100644 --- a/src/Verso.JavaScript/Kernel/JavaScriptKernel.cs +++ b/src/Verso.JavaScript/Kernel/JavaScriptKernel.cs @@ -1,5 +1,6 @@ using Verso.Abstractions; using Verso.JavaScript.MagicCommands; +using Verso.JavaScript.Resources; namespace Verso.JavaScript.Kernel; @@ -30,7 +31,7 @@ public JavaScriptKernel() : this(new JavaScriptKernelOptions()) { } public string Name => "JavaScript"; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "JavaScript language kernel via Node.js or Jint."; + public string? Description => Strings.Kernel_JavaScript_Description; // ILanguageKernel public string LanguageId => "javascript"; @@ -42,14 +43,15 @@ public JavaScriptKernel() : this(new JavaScriptKernelOptions()) { } // IExtensionSettings - public IReadOnlyList SettingDefinitions { get; } = + /// + /// Built on each read rather than held, so the settings panel shows the words in the + /// language the reader asked for rather than the one the kernel first loaded in. + /// + public IReadOnlyList SettingDefinitions => [ - new SettingDefinition(HideInstallOutputSetting, "Hide Installation Output", - "Report an npm install as the packages it added and the versions they came in at, " + - "rather than relaying npm's own narration and its funding and audit footers. " + - "Installing one package brings in its dependencies, and that block is saved into the " + - "notebook alongside the output the cell was actually run for. An install that fails " + - "is always reported in full, as is anything npm's audit found.", + new SettingDefinition(HideInstallOutputSetting, + Strings.Setting_HideInstallOutput_Label, + Strings.Setting_HideInstallOutput_Description, SettingType.Boolean, true, "Packages"), ]; @@ -188,7 +190,7 @@ private async Task> ExecuteCoreAsync(string code, IExe // Crash recovery if (!_runner!.IsAlive && _usingNode && _options.AutoRestartOnCrash) { - outputs.Add(new CellOutput("text/plain", "Node.js process crashed. Restarting...")); + outputs.Add(new CellOutput("text/plain", Strings.Run_NodeCrashed)); await context.WriteOutputAsync(outputs[0]); await _runner.DisposeAsync(); @@ -253,7 +255,7 @@ private async Task> ExecuteCoreAsync(string code, IExe { outputs.Add(new CellOutput( "text/plain", - result.ErrorMessage ?? "Unknown JavaScript error", + result.ErrorMessage ?? Strings.Run_UnknownError, IsError: true, ErrorName: "JavaScriptError", ErrorStackTrace: result.ErrorStack)); diff --git a/src/Verso.JavaScript/Kernel/JintRunner.cs b/src/Verso.JavaScript/Kernel/JintRunner.cs index 3fa1aeae..187ed01f 100644 --- a/src/Verso.JavaScript/Kernel/JintRunner.cs +++ b/src/Verso.JavaScript/Kernel/JintRunner.cs @@ -5,6 +5,7 @@ using Jint.Native; using Jint.Runtime; using Verso.Abstractions; +using Verso.JavaScript.Resources; namespace Verso.JavaScript.Kernel; @@ -108,12 +109,12 @@ public Task ExecuteAsync(string code, CancellationToken ct) catch (TimeoutException) { hasError = true; - errorMessage = "Execution timed out (15 second limit)."; + errorMessage = Strings.Run_TimedOut; } catch (MemoryLimitExceededException) { hasError = true; - errorMessage = "Memory limit exceeded (128 MB)."; + errorMessage = Strings.Run_OutOfMemory; } catch (Exception ex) { diff --git a/src/Verso.JavaScript/Kernel/NodeProcessRunner.cs b/src/Verso.JavaScript/Kernel/NodeProcessRunner.cs index b08caa0a..270575cd 100644 --- a/src/Verso.JavaScript/Kernel/NodeProcessRunner.cs +++ b/src/Verso.JavaScript/Kernel/NodeProcessRunner.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Text.Json; +using Verso.JavaScript.Resources; namespace Verso.JavaScript.Kernel; @@ -66,7 +67,7 @@ public async Task InitializeAsync(CancellationToken ct) } catch (OperationCanceledException) when (!ct.IsCancellationRequested) { - throw new TimeoutException("Node.js bridge did not send ready signal within 10 seconds."); + throw new TimeoutException(Strings.Node_NotReady); } _alive = true; @@ -206,7 +207,7 @@ public async ValueTask DisposeAsync() private async Task SendCommandAsync(object command, CancellationToken ct) { if (!IsAlive) - throw new InvalidOperationException("Node.js process is not running."); + throw new InvalidOperationException(Strings.Node_NotRunning); var json = JsonSerializer.Serialize(command); @@ -262,7 +263,7 @@ private async Task ReadLoopAsync(CancellationToken ct) _alive = false; foreach (var tcs in _pending.Values) - tcs.TrySetException(new IOException("Node.js process terminated unexpectedly.")); + tcs.TrySetException(new IOException(Strings.Node_Terminated)); _pending.Clear(); } diff --git a/src/Verso.JavaScript/Kernel/TypeScriptKernel.cs b/src/Verso.JavaScript/Kernel/TypeScriptKernel.cs index 069e56f4..439e74b2 100644 --- a/src/Verso.JavaScript/Kernel/TypeScriptKernel.cs +++ b/src/Verso.JavaScript/Kernel/TypeScriptKernel.cs @@ -1,5 +1,6 @@ using Verso.Abstractions; using Verso.JavaScript.MagicCommands; +using Verso.JavaScript.Resources; namespace Verso.JavaScript.Kernel; @@ -32,7 +33,7 @@ public TypeScriptKernel() : this(new JavaScriptKernelOptions()) { } public string Name => "TypeScript"; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "TypeScript language kernel via Node.js with automatic transpilation."; + public string? Description => Strings.Kernel_TypeScript_Description; // ILanguageKernel public string LanguageId => "typescript"; @@ -50,7 +51,7 @@ public async Task InitializeAsync() var nodeExe = _options.NodeExecutablePath ?? JavaScriptEngineManager.NodeExecutablePath; if (nodeExe is null) throw new InvalidOperationException( - "TypeScript kernel requires Node.js. Node.js was not found on PATH or in well-known locations."); + Strings.Node_Required); _runner = new NodeProcessRunner(nodeExe, _options); await _runner.InitializeAsync(CancellationToken.None); @@ -161,13 +162,13 @@ private async Task> ExecuteCoreAsync(string code, IExe } catch (Exception ex) { - return [new CellOutput("text/plain", $"Transpilation failed: {ex.Message}", + return [new CellOutput("text/plain", string.Format(Strings.Run_TranspileFailed, ex.Message), IsError: true, ErrorName: "TypeScriptError")]; } if (!transpileResult.Success) { - return [new CellOutput("text/plain", transpileResult.Error ?? "Unknown transpilation error", + return [new CellOutput("text/plain", transpileResult.Error ?? Strings.Run_UnknownTranspileError, IsError: true, ErrorName: "TypeScriptError")]; } @@ -199,7 +200,7 @@ private async Task> ExecuteCoreAsync(string code, IExe { outputs.Add(new CellOutput( "text/plain", - result.ErrorMessage ?? "Unknown JavaScript error", + result.ErrorMessage ?? Strings.Run_UnknownError, IsError: true, ErrorName: "TypeScriptError", ErrorStackTrace: result.ErrorStack)); @@ -243,7 +244,8 @@ private async Task EnsureTypeScriptInstalledAsync(IExecutionContext context, Can await NpmManager.EnsureInitializedAsync(ct); var success = await NpmManager.InstallSilentAsync(TypeScriptInstallSpec, ct); if (!success) - throw new InvalidOperationException($"Failed to install the {TypeScriptInstallSpec} npm package."); + throw new InvalidOperationException( + string.Format(Strings.TypeScript_InstallFailed, TypeScriptInstallSpec)); } // Update NODE_PATH so the bridge can find the module @@ -270,7 +272,8 @@ private async Task EnsureTypeScriptInstalledAsync(IExecutionContext context, Can { var installed = NpmManager.GetInstalledPackageVersion("typescript"); throw new InvalidOperationException( - $"TypeScript compiler not working after install (typescript {installed ?? "version unknown"}): {verify.Error}"); + string.Format(Strings.TypeScript_VerifyFailed, + installed ?? Strings.TypeScript_VersionUnknown, verify.Error)); } } diff --git a/src/Verso.JavaScript/MagicCommands/NpmMagicCommand.cs b/src/Verso.JavaScript/MagicCommands/NpmMagicCommand.cs index 1a9859a8..affffaf5 100644 --- a/src/Verso.JavaScript/MagicCommands/NpmMagicCommand.cs +++ b/src/Verso.JavaScript/MagicCommands/NpmMagicCommand.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.JavaScript.Resources; namespace Verso.JavaScript.MagicCommands; @@ -11,19 +12,19 @@ namespace Verso.JavaScript.MagicCommands; public sealed class NpmMagicCommand : IMagicCommand { public string ExtensionId => "verso.magic.npm"; - string IExtension.Name => "Npm Magic Command"; + string IExtension.Name => Strings.Magic_Npm_Name; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - string? IExtension.Description => "Installs npm packages for use in JavaScript cells."; + string? IExtension.Description => Strings.Extension_Npm_Description; // IMagicCommand public string Name => "npm"; - public string Description => "Installs npm packages for use in subsequent JavaScript cells."; + public string Description => Strings.Magic_Npm_Description; public IReadOnlyList Parameters { get; } = [ new ParameterDefinition("packages", - "One or more package names (e.g. lodash axios@1.6).", + Strings.Magic_Npm_Param_Packages, typeof(string), IsRequired: true), ]; @@ -41,7 +42,7 @@ public async Task ExecuteAsync(string arguments, IMagicCommandContext context) if (string.IsNullOrWhiteSpace(packages)) { await context.WriteOutputAsync(new CellOutput( - "text/plain", "Usage: #!npm ", IsError: true, ErrorName: "NpmError")); + "text/plain", Strings.Magic_Npm_Usage, IsError: true, ErrorName: "NpmError")); context.SuppressExecution = true; return; } @@ -53,7 +54,7 @@ await context.WriteOutputAsync(new CellOutput( { await context.WriteOutputAsync(new CellOutput( "text/plain", - "No JavaScript kernel is loaded. #!npm requires the JavaScript kernel.", + Strings.Magic_Npm_NoKernel, IsError: true, ErrorName: "NpmError")); context.SuppressExecution = true; return; @@ -75,7 +76,7 @@ await context.WriteOutputAsync(new CellOutput( // Said before the install starts rather than after it finishes. Resolving a package with // a large dependency tree takes long enough that a cell showing nothing looks stuck. await context.WriteOutputAsync(new CellOutput( - "text/plain", $"Installing {string.Join(", ", packageNames)}...")); + "text/plain", string.Format(Strings.Magic_Npm_Installing, string.Join(", ", packageNames)))); var detail = (jsKernel as Kernel.JavaScriptKernel)?.ShowInstallOutput ?? false; @@ -100,8 +101,8 @@ private static string AlreadyInstalled(IReadOnlyList packageNames) : name) .ToList(); - return described.Count == 1 - ? $"{described[0]} is already installed." - : $"{string.Join(", ", described)} are already installed."; + return string.Format( + Plural.Of(described.Count, Strings.Magic_Npm_AlreadyInstalled_One, Strings.Magic_Npm_AlreadyInstalled_Other), + string.Join(", ", described)); } } diff --git a/src/Verso.JavaScript/MagicCommands/NpmManager.cs b/src/Verso.JavaScript/MagicCommands/NpmManager.cs index e0b157f6..ad65e662 100644 --- a/src/Verso.JavaScript/MagicCommands/NpmManager.cs +++ b/src/Verso.JavaScript/MagicCommands/NpmManager.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using Verso.Abstractions; +using Verso.JavaScript.Resources; namespace Verso.JavaScript.MagicCommands; @@ -52,7 +53,7 @@ public static async Task InstallAsync( if (npmExe is null) { await context.WriteOutputAsync(new CellOutput( - "text/plain", "npm not found on PATH.", IsError: true, ErrorName: "NpmError")); + "text/plain", Strings.Npm_NotFound, IsError: true, ErrorName: "NpmError")); return false; } @@ -74,7 +75,7 @@ await context.WriteOutputAsync(new CellOutput( if (proc is null) { await context.WriteOutputAsync(new CellOutput( - "text/plain", "Failed to start npm process.", IsError: true, ErrorName: "NpmError")); + "text/plain", Strings.Npm_StartFailed, IsError: true, ErrorName: "NpmError")); return false; } @@ -124,7 +125,7 @@ await context.WriteOutputAsync(new CellOutput( } await context.WriteOutputAsync(new CellOutput( - "text/plain", "The npm install failed.", IsError: true, ErrorName: "NpmError")); + "text/plain", Strings.Npm_InstallFailed, IsError: true, ErrorName: "NpmError")); } /// diff --git a/src/Verso.JavaScript/MagicCommands/NpmReport.cs b/src/Verso.JavaScript/MagicCommands/NpmReport.cs index 559b20b7..0ecceaea 100644 --- a/src/Verso.JavaScript/MagicCommands/NpmReport.cs +++ b/src/Verso.JavaScript/MagicCommands/NpmReport.cs @@ -1,5 +1,7 @@ using System.Text; using System.Text.Json; +using Verso.Abstractions; +using Verso.JavaScript.Resources; namespace Verso.JavaScript.MagicCommands; @@ -152,21 +154,30 @@ private static void ReadPackages(JsonElement root, string property, List(); - foreach (var severity in new[] { "critical", "high", "moderate", "low", "info" }) + foreach (var (severity, phrase) in new[] + { + ("critical", Strings.Npm_Severity_Critical), + ("high", Strings.Npm_Severity_High), + ("moderate", Strings.Npm_Severity_Moderate), + ("low", Strings.Npm_Severity_Low), + ("info", Strings.Npm_Severity_Info), + }) { var found = Number(counts, severity); if (found > 0) - severities.Add($"{found} {severity}"); + severities.Add(string.Format(phrase, found)); } - var summary = new StringBuilder("npm audit found ") - .Append(total == 1 ? "1 vulnerability" : $"{total} vulnerabilities"); - - if (severities.Count > 0) - summary.Append(" (").Append(string.Join(", ", severities)).Append(')'); + var counted = string.Format( + Plural.Of(total, Strings.Npm_VulnerabilityCount_One, Strings.Npm_VulnerabilityCount_Other), + total); - return summary.Append(" in the installed packages. Run npm audit for details.").ToString(); + return severities.Count > 0 + ? string.Format(Strings.Npm_AuditSummaryBySeverity, counted, string.Join(", ", severities)) + : string.Format(Strings.Npm_AuditSummary, counted); } private static string? DescribeError(JsonElement failure) @@ -237,12 +248,12 @@ public IReadOnlyList Summarize(IReadOnlyList requested, bool det return lines; if (named.Count > 0 && rest.Count > 0) - lines.Add("Dependencies: " + Describe(rest) + "."); + lines.Add(string.Format(Strings.Npm_Dependencies, Describe(rest))); else if (named.Count == 0 && Added.Count > 0) - lines.Add("Packages: " + Describe(Added) + "."); + lines.Add(string.Format(Strings.Npm_Packages, Describe(Added))); if (Removed.Count > 0) - lines.Add("Replaced: " + Describe(Removed) + "."); + lines.Add(string.Format(Strings.Npm_Replaced, Describe(Removed))); lines.AddRange(Deprecations); return lines; @@ -254,7 +265,7 @@ private string Headline( IReadOnlyList? requested) { if (Added.Count == 0) - return "Everything requested is already installed."; + return Strings.Npm_NothingToDo; // Nothing added matches what was asked for, which is what installing from a package file // rather than by name looks like from here. @@ -265,24 +276,27 @@ private string Headline( : null; return packages is not null - ? $"Installed {packages} and {Count(rest.Count, "dependency", "dependencies")}." - : $"Installed {Count(rest.Count, "package")}."; + ? string.Format(Strings.Npm_InstalledAnd, packages, Dependencies(rest.Count)) + : string.Format(Strings.Npm_Installed, string.Format( + Plural.Of(rest.Count, Strings.Npm_PackageCount_One, Strings.Npm_PackageCount_Other), + rest.Count)); } - var headline = new StringBuilder("Installed ").Append(Describe(named)); - - if (rest.Count > 0) - headline.Append(" and ").Append(Count(rest.Count, "dependency", "dependencies")); - - return headline.Append('.').ToString(); + // The counts are written out on their own and go in as one argument each, so the + // sentence around them needs one entry rather than a singular and a plural of the whole. + return rest.Count > 0 + ? string.Format(Strings.Npm_InstalledAnd, Describe(named), Dependencies(rest.Count)) + : string.Format(Strings.Npm_Installed, Describe(named)); } + private static string Dependencies(int count) + => string.Format( + Plural.Of(count, Strings.Npm_DependencyCount_One, Strings.Npm_DependencyCount_Other), + count); + private static string Describe(IReadOnlyList packages) => string.Join(", ", packages.OrderBy(p => p.Name, StringComparer.Ordinal).Select(p => p.ToString())); - private static string Count(int count, string singular, string? plural = null) - => count == 1 ? $"1 {singular}" : $"{count} {plural ?? singular + "s"}"; - private static string Text(JsonElement element, string property) => element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.String ? value.GetString() ?? string.Empty diff --git a/src/Verso.JavaScript/Resources/Strings.de.resx b/src/Verso.JavaScript/Resources/Strings.de.resx new file mode 100644 index 00000000..54a79711 --- /dev/null +++ b/src/Verso.JavaScript/Resources/Strings.de.resx @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Installiert npm-Pakete zur Verwendung in JavaScript-Zellen. + + + JavaScript-Sprachkernel über Node.js oder Jint. + + + TypeScript-Sprachkernel über Node.js mit automatischer Transpilierung. + + + {0} ist bereits installiert. + + + {0} sind bereits installiert. + + + Installiert npm-Pakete zur Verwendung in nachfolgenden JavaScript-Zellen. + + + {0} wird installiert... + + + Npm-Magic-Command + + + Es ist kein JavaScript-Kernel geladen. #!npm benötigt den JavaScript-Kernel. + + + Ein oder mehrere Paketnamen (z. B. lodash axios@1.6). + + + Verwendung: #!npm <package-names> + + + Die Node.js-Brücke hat innerhalb von 10 Sekunden kein Bereitschaftssignal gesendet. + + + Der Node.js-Prozess läuft nicht. + + + Der TypeScript-Kernel benötigt Node.js. Node.js wurde weder in PATH noch an den üblichen Orten gefunden. + + + Der Node.js-Prozess wurde unerwartet beendet. + + + Die TypeScript-Transpilierung benötigt Node.js mit installiertem Modul typescript. + + + npm audit hat {0} in den installierten Paketen gefunden. Führen Sie npm audit aus, um Einzelheiten zu erhalten. + + + npm audit hat {0} ({1}) in den installierten Paketen gefunden. Führen Sie npm audit aus, um Einzelheiten zu erhalten. + + + Abhängigkeiten: {0}. + + + {0} Abhängigkeit + + + {0} Abhängigkeiten + + + Die npm-Installation ist fehlgeschlagen. + + + {0} installiert. + + + {0} und {1} installiert. + + + npm wurde in PATH nicht gefunden. + + + Alles Angeforderte ist bereits installiert. + + + {0} Paket + + + {0} Pakete + + + Pakete: {0}. + + + Ersetzt: {0}. + + + {0} kritisch + + + {0} hoch + + + {0} Info + + + {0} niedrig + + + {0} mittel + + + Der npm-Prozess konnte nicht gestartet werden. + + + {0} Sicherheitslücke + + + {0} Sicherheitslücken + + + Der Node.js-Prozess ist abgestürzt. Neustart... + + + Speichergrenze überschritten (128 MB). + + + Zeitüberschreitung der Ausführung (Grenze 15 Sekunden). + + + Die Transpilierung ist fehlgeschlagen: {0} + + + Unbekannter JavaScript-Fehler + + + Unbekannter Transpilierungsfehler + + + Eine npm-Installation als die hinzugefügten Pakete und deren Versionen melden, statt npms eigene Ausgabe samt Finanzierungs- und Audit-Fußzeilen weiterzureichen. Die Installation eines Pakets bringt dessen Abhängigkeiten mit, und dieser Block wird zusammen mit der Ausgabe, für die die Zelle eigentlich ausgeführt wurde, im Notebook gespeichert. Eine fehlgeschlagene Installation wird immer vollständig gemeldet, ebenso alles, was npms Audit gefunden hat. + + + Installationsausgabe ausblenden + + + Das npm-Paket {0} konnte nicht installiert werden. + + + Der TypeScript-Compiler funktioniert nach der Installation nicht (typescript {0}): {1} + + + Version unbekannt + + \ No newline at end of file diff --git a/src/Verso.JavaScript/Resources/Strings.es.resx b/src/Verso.JavaScript/Resources/Strings.es.resx new file mode 100644 index 00000000..401cf40b --- /dev/null +++ b/src/Verso.JavaScript/Resources/Strings.es.resx @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Instala paquetes de npm para usarlos en las celdas de JavaScript. + + + Kernel del lenguaje JavaScript mediante Node.js o Jint. + + + Kernel del lenguaje TypeScript mediante Node.js, con transpilación automática. + + + {0} ya está instalado. + + + {0} ya están instalados. + + + Instala paquetes de npm para usarlos en las celdas de JavaScript siguientes. + + + Instalando {0}... + + + Comando mágico Npm + + + No hay ningún kernel de JavaScript cargado. #!npm necesita el kernel de JavaScript. + + + Uno o más nombres de paquete (por ejemplo, lodash axios@1.6). + + + Uso: #!npm <package-names> + + + El puente de Node.js no envió la señal de listo en 10 segundos. + + + El proceso de Node.js no se está ejecutando. + + + El kernel de TypeScript necesita Node.js. No se encontró Node.js en PATH ni en las ubicaciones habituales. + + + El proceso de Node.js terminó de forma inesperada. + + + La transpilación de TypeScript necesita Node.js con el módulo typescript instalado. + + + npm audit encontró {0} en los paquetes instalados. Ejecute npm audit para ver los detalles. + + + npm audit encontró {0} ({1}) en los paquetes instalados. Ejecute npm audit para ver los detalles. + + + Dependencias: {0}. + + + {0} dependencia + + + {0} dependencias + + + La instalación de npm falló. + + + Se instaló {0}. + + + Se instaló {0} y {1}. + + + No se encontró npm en PATH. + + + Todo lo solicitado ya está instalado. + + + {0} paquete + + + {0} paquetes + + + Paquetes: {0}. + + + Reemplazados: {0}. + + + críticas: {0} + + + altas: {0} + + + informativas: {0} + + + bajas: {0} + + + moderadas: {0} + + + No se pudo iniciar el proceso de npm. + + + {0} vulnerabilidad + + + {0} vulnerabilidades + + + El proceso de Node.js se bloqueó. Reiniciando... + + + Se superó el límite de memoria (128 MB). + + + Se agotó el tiempo de ejecución (límite de 15 segundos). + + + La transpilación falló: {0} + + + Error de JavaScript desconocido + + + Error de transpilación desconocido + + + Informa de una instalación de npm indicando los paquetes que añadió y las versiones con las que llegaron, en lugar de repetir la narración de npm y sus notas de financiación y auditoría. Instalar un paquete arrastra sus dependencias, y ese bloque se guarda en el cuaderno junto a la salida por la que realmente se ejecutó la celda. Una instalación que falla siempre se informa al completo, igual que lo que encuentre la auditoría de npm. + + + Ocultar la salida de instalación + + + No se pudo instalar el paquete npm {0}. + + + El compilador de TypeScript no funciona después de instalarlo (typescript {0}): {1} + + + versión desconocida + + \ No newline at end of file diff --git a/src/Verso.JavaScript/Resources/Strings.ja.resx b/src/Verso.JavaScript/Resources/Strings.ja.resx new file mode 100644 index 00000000..9b62a367 --- /dev/null +++ b/src/Verso.JavaScript/Resources/Strings.ja.resx @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + JavaScript セルで使う npm パッケージをインストールします。 + + + Node.js または Jint による JavaScript 言語カーネル。 + + + Node.js で自動的にトランスパイルを行う TypeScript 言語カーネル。 + + + {0} は既にインストールされています。 + + + {0} は既にインストールされています。 + + + 以降の JavaScript セルで使う npm パッケージをインストールします。 + + + {0} をインストールしています... + + + Npm マジックコマンド + + + JavaScript カーネルが読み込まれていません。#!npm には JavaScript カーネルが必要です。 + + + 1 つ以上のパッケージ名 (例: lodash axios@1.6)。 + + + 使い方: #!npm <package-names> + + + Node.js のブリッジが 10 秒以内に準備完了を通知しませんでした。 + + + Node.js のプロセスが実行されていません。 + + + TypeScript カーネルには Node.js が必要です。Node.js が PATH にも既知の場所にも見つかりませんでした。 + + + Node.js のプロセスが予期せず終了しました。 + + + TypeScript のトランスパイルには、typescript モジュールがインストールされた Node.js が必要です。 + + + npm audit がインストール済みのパッケージに {0} を検出しました。詳しくは npm audit を実行してください。 + + + npm audit がインストール済みのパッケージに {0} ({1}) を検出しました。詳しくは npm audit を実行してください。 + + + 依存関係: {0}。 + + + {0} 件の依存関係 + + + {0} 件の依存関係 + + + npm のインストールに失敗しました。 + + + {0} をインストールしました。 + + + {0} と {1} をインストールしました。 + + + npm が PATH に見つかりません。 + + + 要求されたものはすべてインストール済みです。 + + + {0} 個のパッケージ + + + {0} 個のパッケージ + + + パッケージ: {0}。 + + + 置き換え: {0}。 + + + 緊急 {0} 件 + + + 高 {0} 件 + + + 情報 {0} 件 + + + 低 {0} 件 + + + 中 {0} 件 + + + npm のプロセスを開始できませんでした。 + + + {0} 件の脆弱性 + + + {0} 件の脆弱性 + + + Node.js のプロセスがクラッシュしました。再起動しています... + + + メモリの上限 (128 MB) を超えました。 + + + 実行が制限時間 (15 秒) を超えました。 + + + トランスパイルに失敗しました: {0} + + + 不明な JavaScript エラー + + + 不明なトランスパイルエラー + + + npm のインストールを、追加されたパッケージとそれぞれのバージョンとして報告し、npm 自身の経過表示や funding と audit のフッターは中継しません。パッケージを 1 つインストールすると依存関係も入り、その一連の出力が、セルを実行して本来得たかった出力と一緒にノートブックへ保存されます。失敗したインストールは常に全体を報告します。npm audit が検出した内容も同じです。 + + + インストール出力を隠す + + + npm パッケージ {0} をインストールできませんでした。 + + + インストール後も TypeScript コンパイラが動作しません (typescript {0}): {1} + + + バージョン不明 + + \ No newline at end of file diff --git a/src/Verso.JavaScript/Resources/Strings.qps-Ploc.resx b/src/Verso.JavaScript/Resources/Strings.qps-Ploc.resx new file mode 100644 index 00000000..dd5f84ab --- /dev/null +++ b/src/Verso.JavaScript/Resources/Strings.qps-Ploc.resx @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + [!!Ïñštàllš ñpm pàçkàgéš fòr ùšé ïñ JàvàŠçrïpt çéllš.···!!] + + + [!!JàvàŠçrïpt làñgùàgé kérñél vïà Ñòdé.jš òr Jïñt.···!!] + + + [!!TÿpéŠçrïpt làñgùàgé kérñél vïà Ñòdé.jš wïth àùtòmàtïç tràñšpïlàtïòñ.···!!] + + + [!!{0} ïš àlréàdÿ ïñštàlléd.···!!] + + + [!!{0} àré àlréàdÿ ïñštàlléd.···!!] + + + [!!Ïñštàllš ñpm pàçkàgéš fòr ùšé ïñ šùbšéqùéñt JàvàŠçrïpt çéllš.···!!] + + + [!!Ïñštàllïñg {0}...···!!] + + + [!!Ñpm Màgïç Çòmmàñd···!!] + + + [!!Ñò JàvàŠçrïpt kérñél ïš lòàdéd. #!ñpm réqùïréš thé JàvàŠçrïpt kérñél.···!!] + + + [!!Òñé òr mòré pàçkàgé ñàméš (é.g. lòdàšh àxïòš@1.6).···!!] + + + [!!Ùšàgé: #!ñpm <pàçkàgé-ñàméš>···!!] + + + [!!Ñòdé.jš brïdgé dïd ñòt šéñd réàdÿ šïgñàl wïthïñ 10 šéçòñdš.···!!] + + + [!!Ñòdé.jš pròçéšš ïš ñòt rùññïñg.···!!] + + + [!!TÿpéŠçrïpt kérñél réqùïréš Ñòdé.jš. Ñòdé.jš wàš ñòt fòùñd òñ PÀTH òr ïñ wéll-kñòwñ lòçàtïòñš.···!!] + + + [!!Ñòdé.jš pròçéšš térmïñàtéd ùñéxpéçtédlÿ.···!!] + + + [!!TÿpéŠçrïpt tràñšpïlàtïòñ réqùïréš Ñòdé.jš wïth thé tÿpéšçrïpt mòdùlé ïñštàlléd.···!!] + + + [!!ñpm àùdït fòùñd {0} ïñ thé ïñštàlléd pàçkàgéš. Rùñ ñpm àùdït fòr détàïlš.···!!] + + + [!!ñpm àùdït fòùñd {0} ({1}) ïñ thé ïñštàlléd pàçkàgéš. Rùñ ñpm àùdït fòr détàïlš.···!!] + + + [!!Dépéñdéñçïéš: {0}.···!!] + + + [!!{0} dépéñdéñçÿ···!!] + + + [!!{0} dépéñdéñçïéš···!!] + + + [!!Thé ñpm ïñštàll fàïléd.···!!] + + + [!!Ïñštàlléd {0}.···!!] + + + [!!Ïñštàlléd {0} àñd {1}.···!!] + + + [!!ñpm ñòt fòùñd òñ PÀTH.···!!] + + + [!!Évérÿthïñg réqùéštéd ïš àlréàdÿ ïñštàlléd.···!!] + + + [!!{0} pàçkàgé···!!] + + + [!!{0} pàçkàgéš···!!] + + + [!!Pàçkàgéš: {0}.···!!] + + + [!!Réplàçéd: {0}.···!!] + + + [!!{0} çrïtïçàl···!!] + + + [!!{0} hïgh···!!] + + + [!!{0} ïñfò···!!] + + + [!!{0} lòw···!!] + + + [!!{0} mòdéràté···!!] + + + [!!Fàïléd tò štàrt ñpm pròçéšš.···!!] + + + [!!{0} vùlñéràbïlïtÿ···!!] + + + [!!{0} vùlñéràbïlïtïéš···!!] + + + [!!Ñòdé.jš pròçéšš çràšhéd. Réštàrtïñg...···!!] + + + [!!Mémòrÿ lïmït éxçéédéd (128 MB).···!!] + + + [!!Éxéçùtïòñ tïméd òùt (15 šéçòñd lïmït).···!!] + + + [!!Tràñšpïlàtïòñ fàïléd: {0}···!!] + + + [!!Ùñkñòwñ JàvàŠçrïpt érròr···!!] + + + [!!Ùñkñòwñ tràñšpïlàtïòñ érròr···!!] + + + [!!Répòrt àñ ñpm ïñštàll àš thé pàçkàgéš ït àddéd àñd thé véršïòñš théÿ çàmé ïñ àt, ràthér thàñ rélàÿïñg ñpm'š òwñ ñàrràtïòñ àñd ïtš fùñdïñg àñd àùdït fòòtérš. Ïñštàllïñg òñé pàçkàgé brïñgš ïñ ïtš dépéñdéñçïéš, àñd thàt blòçk ïš šàvéd ïñtò thé ñòtébòòk àlòñgšïdé thé òùtpùt thé çéll wàš àçtùàllÿ rùñ fòr. Àñ ïñštàll thàt fàïlš ïš àlwàÿš répòrtéd ïñ fùll, àš ïš àñÿthïñg ñpm'š àùdït fòùñd.···!!] + + + [!!Hïdé Ïñštàllàtïòñ Òùtpùt···!!] + + + [!!Fàïléd tò ïñštàll thé {0} ñpm pàçkàgé.···!!] + + + [!!TÿpéŠçrïpt çòmpïlér ñòt wòrkïñg àftér ïñštàll (tÿpéšçrïpt {0}): {1}···!!] + + + [!!véršïòñ ùñkñòwñ···!!] + + \ No newline at end of file diff --git a/src/Verso.JavaScript/Resources/Strings.resx b/src/Verso.JavaScript/Resources/Strings.resx new file mode 100644 index 00000000..4a28c635 --- /dev/null +++ b/src/Verso.JavaScript/Resources/Strings.resx @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Installs npm packages for use in JavaScript cells. + What the #!npm extension is, shown wherever extensions are listed. + + + JavaScript language kernel via Node.js or Jint. + What the JavaScript kernel is, shown wherever kernels are listed. + + + TypeScript language kernel via Node.js with automatic transpilation. + What the TypeScript kernel is, shown wherever kernels are listed. + + + {0} is already installed. + Nothing was done. {0} is one package name, with its version after it when that is known. + + + {0} are already installed. + The same for more than one. {0} is a comma-separated list. + + + Installs npm packages for use in subsequent JavaScript cells. + What #!npm does, shown in the list of magic commands. + + + Installing {0}... + Printed before the install starts, so a slow one does not look like nothing happening. {0} is a comma-separated list of package names. + + + Npm Magic Command + Name of the #!npm magic command as an extension, as listed in the Extensions panel. The command word itself is typed into a cell and is not translated. + + + No JavaScript kernel is loaded. #!npm requires the JavaScript kernel. + #!npm installs for the JavaScript kernel, and there is not one to install for. + + + One or more package names (e.g. lodash axios@1.6). + What the argument to #!npm is. The two package names are examples and stay as written. + + + Usage: #!npm <package-names> + Shown when #!npm is given nothing to install. Everything after 'Usage:' is typed at a keyboard and stays as written. + + + Node.js bridge did not send ready signal within 10 seconds. + Node.js started but never answered, so the cell cannot run. + + + Node.js process is not running. + A cell was run after the process behind it had stopped. + + + TypeScript kernel requires Node.js. Node.js was not found on PATH or in well-known locations. + TypeScript cells cannot run without Node.js installed. PATH is the name of an environment variable and stays as written. + + + Node.js process terminated unexpectedly. + The process running the cell stopped part-way through. + + + TypeScript transpilation requires Node.js with the typescript module installed. + The built-in engine cannot compile TypeScript. 'typescript' is a package name and stays as written. + + + npm audit found {0} in the installed packages. Run npm audit for details. + {0} is a count already written out. 'npm audit' is a command typed at a keyboard and stays as written. + + + npm audit found {0} ({1}) in the installed packages. Run npm audit for details. + {0} is a count already written out, {1} a comma-separated breakdown such as '2 high, 1 low'. + + + Dependencies: {0}. + Names what came along with the request. {0} is a comma-separated list with versions. + + + {0} dependency + How many packages came along with what was asked for, written for exactly one. Goes into the sentence above. + + + {0} dependencies + The same for any other number. + + + The npm install failed. + Shown when the install failed and npm said nothing about why. + + + Installed {0}. + {0} is a list of packages with their versions, or a count already written out. + + + Installed {0} and {1}. + {0} is what was asked for, {1} a count of what came along with it, already written out. + + + npm not found on PATH. + npm is not installed, or not where it can be found. PATH is the name of an environment variable and stays as written. + + + Everything requested is already installed. + The install ran and added nothing, because it was all there. + + + {0} package + How many packages were installed, written for exactly one. + + + {0} packages + The same for any other number. + + + Packages: {0}. + Names what was installed when none of it matches what was asked for by name. {0} is a comma-separated list with versions. + + + Replaced: {0}. + Names what the install took out. {0} is a comma-separated list with versions. + + + {0} critical + How many of npm's audit findings were rated critical. {0} is the count. Goes into the breakdown in the sentence above. + + + {0} high + How many of npm's audit findings were rated high. {0} is the count. Goes into the breakdown in the sentence above. + + + {0} info + How many of npm's audit findings were rated info. {0} is the count. Goes into the breakdown in the sentence above. + + + {0} low + How many of npm's audit findings were rated low. {0} is the count. Goes into the breakdown in the sentence above. + + + {0} moderate + How many of npm's audit findings were rated moderate. {0} is the count. Goes into the breakdown in the sentence above. + + + Failed to start npm process. + npm was found but would not run. + + + {0} vulnerability + How many problems npm's audit found, written for exactly one. Goes into the sentence below. + + + {0} vulnerabilities + The same for any other number. + + + Node.js process crashed. Restarting... + The process running the cell stopped on its own and is being started again. + + + Memory limit exceeded (128 MB). + A cell asked the built-in engine for more memory than it is allowed. + + + Execution timed out (15 second limit). + A cell ran on the built-in engine for longer than it is allowed to. + + + Transpilation failed: {0} + TypeScript could not be turned into JavaScript. {0} is the underlying error, which arrives in English. + + + Unknown JavaScript error + Shown in place of an error message when the engine reported a failure without saying what it was. + + + Unknown transpilation error + Shown in place of an error message when TypeScript would not compile and said nothing about why. + + + Report an npm install as the packages it added and the versions they came in at, rather than relaying npm's own narration and its funding and audit footers. Installing one package brings in its dependencies, and that block is saved into the notebook alongside the output the cell was actually run for. An install that fails is always reported in full, as is anything npm's audit found. + What the setting above does, shown under its name in the settings panel. + + + Hide Installation Output + Name of a setting, shown in the settings panel. Keep it short; it sits on one line beside a switch. + + + Failed to install the {0} npm package. + {0} is the package and version being installed, such as typescript@5, which stays as written. + + + TypeScript compiler not working after install (typescript {0}): {1} + The compiler installed but will not compile anything. {0} is the version that installed, {1} the underlying error, which arrives in English. + + + version unknown + Stands in for the version number in the message above when it could not be read. + + \ No newline at end of file diff --git a/src/Verso.JavaScript/Resources/Strings.zh-Hans.resx b/src/Verso.JavaScript/Resources/Strings.zh-Hans.resx new file mode 100644 index 00000000..117aa8a7 --- /dev/null +++ b/src/Verso.JavaScript/Resources/Strings.zh-Hans.resx @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 安装 npm 包以供 JavaScript 单元格使用。 + + + 通过 Node.js 或 Jint 运行的 JavaScript 语言内核。 + + + 通过 Node.js 运行并自动转译的 TypeScript 语言内核。 + + + {0} 已安装。 + + + {0} 已安装。 + + + 安装 npm 包以供后续 JavaScript 单元格使用。 + + + 正在安装 {0}... + + + Npm 魔法命令 + + + 未加载 JavaScript 内核。#!npm 需要 JavaScript 内核。 + + + 一个或多个包名称(例如 lodash axios@1.6)。 + + + 用法:#!npm <package-names> + + + Node.js 桥接在 10 秒内未发送就绪信号。 + + + Node.js 进程未在运行。 + + + TypeScript 内核需要 Node.js。在 PATH 或常见位置中都找不到 Node.js。 + + + Node.js 进程意外终止。 + + + TypeScript 转译需要 Node.js 并安装 typescript 模块。 + + + npm audit 在已安装的包中发现 {0}。运行 npm audit 了解详细信息。 + + + npm audit 在已安装的包中发现 {0}({1})。运行 npm audit 了解详细信息。 + + + 依赖项:{0}。 + + + {0} 个依赖项 + + + {0} 个依赖项 + + + npm 安装失败。 + + + 已安装 {0}。 + + + 已安装 {0} 和 {1}。 + + + 在 PATH 中找不到 npm。 + + + 请求的内容均已安装。 + + + {0} 个包 + + + {0} 个包 + + + 包:{0}。 + + + 已替换:{0}。 + + + 严重 {0} 个 + + + 高 {0} 个 + + + 信息 {0} 个 + + + 低 {0} 个 + + + 中 {0} 个 + + + 无法启动 npm 进程。 + + + {0} 个漏洞 + + + {0} 个漏洞 + + + Node.js 进程已崩溃。正在重启... + + + 超出内存限制(128 MB)。 + + + 执行超时(15 秒限制)。 + + + 转译失败:{0} + + + 未知的 JavaScript 错误 + + + 未知的转译错误 + + + 以安装了哪些包及其版本来报告 npm 安装结果,而不是转述 npm 自己的过程叙述及其赞助和审核页脚。安装一个包会带入它的依赖项,而这段内容会与该单元格实际运行所得的输出一起保存进笔记本。安装失败时始终完整报告,npm audit 的发现也是如此。 + + + 隐藏安装输出 + + + 无法安装 {0} npm 包。 + + + 安装后 TypeScript 编译器无法工作(typescript {0}):{1} + + + 版本未知 + + \ No newline at end of file diff --git a/src/Verso.JavaScript/Verso.JavaScript.csproj b/src/Verso.JavaScript/Verso.JavaScript.csproj index 2c73e5a8..bc323ba4 100644 --- a/src/Verso.JavaScript/Verso.JavaScript.csproj +++ b/src/Verso.JavaScript/Verso.JavaScript.csproj @@ -15,6 +15,23 @@ + + + + MSBuild:Compile + $(IntermediateOutputPath)Strings.Designer.cs + CSharp + Verso.JavaScript.Resources + Strings + + + diff --git a/src/Verso.PowerShell/Kernel/PowerShellKernel.cs b/src/Verso.PowerShell/Kernel/PowerShellKernel.cs index 10ea912b..98e2c331 100644 --- a/src/Verso.PowerShell/Kernel/PowerShellKernel.cs +++ b/src/Verso.PowerShell/Kernel/PowerShellKernel.cs @@ -1,5 +1,6 @@ using Verso.Abstractions; using Verso.PowerShell.Kernel.Host; +using Verso.PowerShell.Resources; namespace Verso.PowerShell.Kernel; @@ -19,7 +20,7 @@ public sealed class PowerShellKernel : ILanguageKernel public string Name => "PowerShell"; public string Version => "1.0.0"; public string? Author => "Datafication"; - public string? Description => "PowerShell language kernel powered by System.Management.Automation."; + public string? Description => Strings.Kernel_Description; // ILanguageKernel public string LanguageId => "powershell"; diff --git a/src/Verso.PowerShell/Kernel/RunspaceManager.cs b/src/Verso.PowerShell/Kernel/RunspaceManager.cs index 55ee7594..7786a60f 100644 --- a/src/Verso.PowerShell/Kernel/RunspaceManager.cs +++ b/src/Verso.PowerShell/Kernel/RunspaceManager.cs @@ -6,6 +6,7 @@ using System.Text; using Verso.Abstractions; using Verso.PowerShell.Kernel.Host; +using Verso.PowerShell.Resources; namespace Verso.PowerShell.Kernel; @@ -448,14 +449,31 @@ public static IReadOnlyList GetDiagnostics(string code) } sb.Append("
    "); - sb.Append("
    ") - .Append(dataRowCount.ToString("N0")) - .Append(" object(s)
    "); + AppendObjectCountFooter(sb, dataRowCount); sb.Append("
    "); return sb.ToString(); } + /// + /// Writes the line under a table saying how much of a result it is showing. + /// + /// + /// The count goes in as an argument rather than being written beside the word, because the + /// number and the noun it counts do not sit in the same order in every language, and because + /// the singular and the plural are separate words in most of them. + /// + private static void AppendObjectCountFooter(StringBuilder sb, int count) + { + var described = string.Format( + Plural.Of(count, Strings.Table_ObjectCount_One, Strings.Table_ObjectCount_Other), + count.ToString("N0")); + + sb.Append("
    ") + .Append(WebUtility.HtmlEncode(described)) + .Append("
    "); + } + private static void AppendTableStyles(StringBuilder sb) { sb.Append(""); if (hasMermaid) @@ -306,7 +307,8 @@ private static void WriteTruncated(StringBuilder sb, string content, bool previe sb.AppendLine(WebUtility.HtmlEncode(lines[i])); } var omitted = lines.Length - previewLineCount; - sb.Append("... (").Append(omitted).AppendLine(omitted == 1 ? " more line)" : " more lines)"); + sb.AppendLine(string.Format( + Plural.Of(omitted, Strings.Export_MoreLines_One, Strings.Export_MoreLines_Other), omitted)); } else { diff --git a/src/Verso/Export/NotebookMarkdownExporter.cs b/src/Verso/Export/NotebookMarkdownExporter.cs index 399e5a1f..16b6e303 100644 --- a/src/Verso/Export/NotebookMarkdownExporter.cs +++ b/src/Verso/Export/NotebookMarkdownExporter.cs @@ -2,6 +2,7 @@ using Verso.Abstractions; using Verso.Extensions.Layouts; using Verso.Extensions.Utilities; +using Verso.Resources; namespace Verso.Export; @@ -203,7 +204,10 @@ private static void WritePreviewableLines(StringBuilder sb, string content, bool sb.Append("> ").AppendLine(lines[i]); } var omitted = lines.Length - previewLineCount; - sb.Append("> ... (").Append(omitted).AppendLine(omitted == 1 ? " more line)" : " more lines)"); + // The quote marker is Markdown rather than words, so it is written here and the sentence + // after it is the same one the HTML export uses. + sb.Append("> ").AppendLine(string.Format( + Plural.Of(omitted, Strings.Export_MoreLines_One, Strings.Export_MoreLines_Other), omitted)); } else { diff --git a/src/Verso/Export/ThemeCssGenerator.cs b/src/Verso/Export/ThemeCssGenerator.cs deleted file mode 100644 index 2456bb77..00000000 --- a/src/Verso/Export/ThemeCssGenerator.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Reflection; -using System.Text; -using Verso.Abstractions; - -namespace Verso.Export; - -/// -/// Generates CSS custom property blocks from Verso theme tokens. -/// Extracted from the Blazor ThemeProvider logic for use in self-contained HTML export. -/// -internal static class ThemeCssGenerator -{ - /// - /// Builds a :root { --verso-xxx: value; } CSS block from the given theme, - /// or from default tokens when is null. - /// - public static string BuildCss(ITheme? theme) - { - var sb = new StringBuilder(); - sb.AppendLine(":root {"); - - // Color tokens - var colors = theme?.Colors ?? new ThemeColorTokens(); - foreach (var prop in typeof(ThemeColorTokens).GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - if (prop.PropertyType != typeof(string)) continue; - var value = (string?)prop.GetValue(colors) ?? ""; - var cssName = ToKebabCase(prop.Name); - sb.AppendLine($" --verso-{cssName}: {value};"); - } - - // Typography tokens - var typography = theme?.Typography ?? new ThemeTypography(); - foreach (var prop in typeof(ThemeTypography).GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - if (prop.PropertyType != typeof(FontDescriptor)) continue; - var font = (FontDescriptor?)prop.GetValue(typography); - if (font is null) continue; - var cssName = ToKebabCase(prop.Name); - sb.AppendLine($" --verso-{cssName}-family: {font.Family};"); - sb.AppendLine($" --verso-{cssName}-size: {font.SizePx}px;"); - sb.AppendLine($" --verso-{cssName}-weight: {font.Weight};"); - sb.AppendLine($" --verso-{cssName}-line-height: {font.LineHeight};"); - } - - // Spacing tokens - var spacing = theme?.Spacing ?? new ThemeSpacing(); - foreach (var prop in typeof(ThemeSpacing).GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - if (prop.PropertyType != typeof(double)) continue; - var value = (double)prop.GetValue(spacing)!; - var cssName = ToKebabCase(prop.Name); - sb.AppendLine($" --verso-{cssName}: {value}px;"); - } - - // Elevation tokens - var elevation = theme?.Elevation ?? new ThemeElevation(); - foreach (var prop in typeof(ThemeElevation).GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - if (prop.PropertyType != typeof(string)) continue; - var value = (string?)prop.GetValue(elevation); - if (value is null) continue; - sb.AppendLine($" --verso-elevation-{ToElevationSuffix(prop.Name)}: {value};"); - } - - sb.AppendLine("}"); - return sb.ToString(); - } - - /// - /// Elevation properties are named Level0..Level3; the prefix is dropped so - /// stylesheets read var(--verso-elevation-1). - /// - internal static string ToElevationSuffix(string propertyName) => - ToKebabCase(propertyName.StartsWith("Level", StringComparison.Ordinal) - ? propertyName["Level".Length..] - : propertyName); - - internal static string ToKebabCase(string name) - { - var sb = new StringBuilder(); - for (int i = 0; i < name.Length; i++) - { - var c = name[i]; - if (char.IsUpper(c)) - { - if (i > 0) sb.Append('-'); - sb.Append(char.ToLowerInvariant(c)); - } - else - { - sb.Append(c); - } - } - return sb.ToString(); - } -} diff --git a/src/Verso/Extensions/CellDisplayPropertyProvider.cs b/src/Verso/Extensions/CellDisplayPropertyProvider.cs index 0f34e208..45716ff3 100644 --- a/src/Verso/Extensions/CellDisplayPropertyProvider.cs +++ b/src/Verso/Extensions/CellDisplayPropertyProvider.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions; @@ -7,10 +8,10 @@ namespace Verso.Extensions; public sealed class CellDisplayPropertyProvider : ICellPropertyProvider { public string ExtensionId => CellViewStateMetadata.ProviderExtensionId; - public string Name => "Cell Display Properties"; + public string Name => Strings.PropertyProvider_Display; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Provides per-cell input and output display settings."; + public string? Description => Strings.PropertyProvider_Display_Description; public int Order => 10; @@ -27,28 +28,28 @@ public Task GetPropertiesSectionAsync(CellModel cell, ICellRend { fields.Add(new( CellViewStateMetadata.InputCollapsedProperty, - "Collapse input", + Strings.Properties_CollapseInput, PropertyFieldType.Toggle, ReadBool(cell, CellViewStateMetadata.InputCollapsedKey))); } fields.Add(new( CellViewStateMetadata.OutputVisibilityProperty, - "Output", + Strings.Properties_Output, PropertyFieldType.Select, ReadOutputVisibility(cell), Options: new[] { - new PropertyFieldOption(CellViewStateMetadata.OutputExpanded, "Full"), - new PropertyFieldOption(CellViewStateMetadata.OutputPreview, "Preview"), - new PropertyFieldOption(CellViewStateMetadata.OutputHidden, "Hidden"), + new PropertyFieldOption(CellViewStateMetadata.OutputExpanded, Strings.Properties_OutputFull), + new PropertyFieldOption(CellViewStateMetadata.OutputPreview, Strings.Properties_OutputPreview), + new PropertyFieldOption(CellViewStateMetadata.OutputHidden, Strings.Properties_OutputHidden), })); if (SupportsInputCollapse(cell)) { fields.Add(new( CellViewStateMetadata.InputPreviewLineCountProperty, - "Input preview lines", + Strings.Properties_InputPreviewLines, PropertyFieldType.Number, ReadPositiveInt( cell, @@ -58,7 +59,7 @@ public Task GetPropertiesSectionAsync(CellModel cell, ICellRend fields.Add(new( CellViewStateMetadata.OutputPreviewLineCountProperty, - "Output preview lines", + Strings.Properties_OutputPreviewLines, PropertyFieldType.Number, ReadPositiveInt( cell, @@ -67,15 +68,15 @@ public Task GetPropertiesSectionAsync(CellModel cell, ICellRend fields.Add(new( CellViewStateMetadata.PreviewStyleProperty, - "Preview style", + Strings.Properties_PreviewStyle, PropertyFieldType.Select, ReadPreviewStyle(cell), Options: new[] { - new PropertyFieldOption(CellViewStateMetadata.PreviewStyleLines, "Lines"), + new PropertyFieldOption(CellViewStateMetadata.PreviewStyleLines, Strings.Properties_PreviewStyleLines), })); - return Task.FromResult(new PropertySection("Display", null, fields)); + return Task.FromResult(new PropertySection(Strings.Properties_DisplaySection, null, fields)); } // Matches Cell.razor's SupportsInputCollapse: only code cells render the gutter chevron diff --git a/src/Verso/Extensions/CellTypes/HtmlCellType.cs b/src/Verso/Extensions/CellTypes/HtmlCellType.cs index 72ed5d40..b89843dd 100644 --- a/src/Verso/Extensions/CellTypes/HtmlCellType.cs +++ b/src/Verso/Extensions/CellTypes/HtmlCellType.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; using Verso.Extensions.Kernels; using Verso.Extensions.Renderers; @@ -14,10 +15,10 @@ public sealed class HtmlCellType : ICellType // --- IExtension --- public string ExtensionId => "verso.celltype.html"; - public string Name => "HTML Cell Type"; + public string Name => Strings.CellType_Html; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "HTML cell type for authoring raw HTML with @variable substitution."; + public string? Description => Strings.CellType_Html_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -25,6 +26,7 @@ public sealed class HtmlCellType : ICellType // --- ICellType --- public string CellTypeId => "html"; + // Format and product names read the same in every language. public string DisplayName => "HTML"; public string? Icon => "" @@ -35,5 +37,8 @@ public sealed class HtmlCellType : ICellType public ILanguageKernel? Kernel { get; } = new HtmlKernel(); public bool IsEditable => true; + // Starter text, not interface text. What this returns is written into the cell and + // saved with the notebook, so it stays as one language rather than depending on who + // happened to add the cell. public string GetDefaultContent() => "\n

    Hello World

    "; } diff --git a/src/Verso/Extensions/CellTypes/MarkdownCellType.cs b/src/Verso/Extensions/CellTypes/MarkdownCellType.cs index f3a78ba5..b5c684b2 100644 --- a/src/Verso/Extensions/CellTypes/MarkdownCellType.cs +++ b/src/Verso/Extensions/CellTypes/MarkdownCellType.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; using Verso.Extensions.Renderers; namespace Verso.Extensions.CellTypes; @@ -14,10 +15,10 @@ public sealed class MarkdownCellType : ICellType // --- IExtension --- public string ExtensionId => "verso.celltype.markdown"; - public string Name => "Markdown Cell Type"; + public string Name => Strings.CellType_Markdown; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Markdown prose cells rendered to HTML with Markdig."; + public string? Description => Strings.CellType_Markdown_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -25,6 +26,7 @@ public sealed class MarkdownCellType : ICellType // --- ICellType --- public string CellTypeId => "markdown"; + // Format and product names read the same in every language. public string DisplayName => "Markdown"; public string? Icon => "" diff --git a/src/Verso/Extensions/CellTypes/MermaidCellType.cs b/src/Verso/Extensions/CellTypes/MermaidCellType.cs index debb6d6c..0d77e5a2 100644 --- a/src/Verso/Extensions/CellTypes/MermaidCellType.cs +++ b/src/Verso/Extensions/CellTypes/MermaidCellType.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; using Verso.Extensions.Kernels; using Verso.Extensions.Renderers; @@ -14,10 +15,10 @@ public sealed class MermaidCellType : ICellType // --- IExtension --- public string ExtensionId => "verso.celltype.mermaid"; - public string Name => "Mermaid Cell Type"; + public string Name => Strings.CellType_Mermaid; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Mermaid cell type for creating diagrams with mermaid.js syntax."; + public string? Description => Strings.CellType_Mermaid_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -25,6 +26,7 @@ public sealed class MermaidCellType : ICellType // --- ICellType --- public string CellTypeId => "mermaid"; + // Format and product names read the same in every language. public string DisplayName => "Mermaid"; public string? Icon => "" @@ -35,5 +37,8 @@ public sealed class MermaidCellType : ICellType public ILanguageKernel? Kernel { get; } = new MermaidKernel(); public bool IsEditable => true; + // Starter text, not interface text. What this returns is written into the cell and + // saved with the notebook, so it stays as one language rather than depending on who + // happened to add the cell. public string GetDefaultContent() => "graph TD\n A[Start] --> B[End]"; } diff --git a/src/Verso/Extensions/CellTypes/ParametersCellType.cs b/src/Verso/Extensions/CellTypes/ParametersCellType.cs index 1615130b..49632fc5 100644 --- a/src/Verso/Extensions/CellTypes/ParametersCellType.cs +++ b/src/Verso/Extensions/CellTypes/ParametersCellType.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; using Verso.Extensions.Renderers; namespace Verso.Extensions.CellTypes; @@ -12,16 +13,16 @@ namespace Verso.Extensions.CellTypes; public sealed class ParametersCellType : ICellType { public string ExtensionId => "verso.celltype.parameters"; - public string Name => "Parameters Cell Type"; + public string Name => Strings.CellType_Parameters; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Displays and manages notebook parameter definitions as an interactive form."; + public string? Description => Strings.CellType_Parameters_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; public string CellTypeId => "parameters"; - public string DisplayName => "Parameters"; + public string DisplayName => Strings.CellType_Parameters_Label; public string? Icon => "" + "" diff --git a/src/Verso/Extensions/CellVisibilityPropertyProvider.cs b/src/Verso/Extensions/CellVisibilityPropertyProvider.cs index 50e1d1f0..59dfce4b 100644 --- a/src/Verso/Extensions/CellVisibilityPropertyProvider.cs +++ b/src/Verso/Extensions/CellVisibilityPropertyProvider.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions; @@ -19,10 +20,10 @@ public sealed class CellVisibilityPropertyProvider : ICellPropertyProvider // --- IExtension --- public string ExtensionId => "verso.propertyprovider.visibility"; - public string Name => "Cell Visibility Properties"; + public string Name => Strings.PropertyProvider_Visibility; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Provides per-layout cell visibility overrides in the properties panel."; + public string? Description => Strings.PropertyProvider_Visibility_Description; // --- ICellPropertyProvider --- @@ -62,11 +63,11 @@ public Task GetPropertiesSectionAsync(CellModel cell, ICellRend continue; var currentValue = ReadOverride(cell, layout.LayoutId); - var defaultState = MapHintToDefaultStateName(defaultHint, supported); + var defaultState = DefaultState(defaultHint, supported); var options = supported .OrderBy(s => s) - .Select(s => new PropertyFieldOption(s.ToString().ToLowerInvariant(), FormatStateName(s))) + .Select(s => new PropertyFieldOption(OptionValue(s), FormatStateName(s))) .ToList(); fields.Add(new PropertyField( @@ -74,11 +75,11 @@ public Task GetPropertiesSectionAsync(CellModel cell, ICellRend DisplayName: layout.DisplayName, FieldType: PropertyFieldType.Select, CurrentValue: currentValue, - Description: $"Default: {defaultState}", + Description: string.Format(Strings.Properties_DefaultIs, FormatStateName(defaultState)), Options: options)); } - var section = new PropertySection("Visibility", null, fields); + var section = new PropertySection(Strings.Properties_VisibilitySection, null, fields); return Task.FromResult(section); } @@ -99,10 +100,10 @@ public Task OnPropertyChangedAsync(CellModel cell, string propertyName, object? var layout = layouts.FirstOrDefault(l => l.LayoutId == layoutId); var supported = layout?.SupportedVisibilityStates ?? new HashSet { CellVisibilityState.Visible }; - var defaultStateName = MapHintToDefaultStateName(defaultHint, supported).ToLowerInvariant(); + var defaultStateValue = OptionValue(DefaultState(defaultHint, supported)); var isDefault = string.IsNullOrEmpty(stringValue) || - string.Equals(stringValue, defaultStateName, StringComparison.OrdinalIgnoreCase); + string.Equals(stringValue, defaultStateValue, StringComparison.OrdinalIgnoreCase); // Get or create the visibility dictionary, handling all storage forms: // - Dictionary from in-memory edits @@ -172,25 +173,39 @@ public Task OnPropertyChangedAsync(CellModel cell, string propertyName, object? } } - private static string MapHintToDefaultStateName( + /// + /// The state a cell falls back to under a layout when nothing has been chosen for it. + /// + /// + /// Returns the state itself rather than a name for it. Deciding what the default is and + /// writing it out are two jobs: the first is compared against a stored value, the second + /// is read by a person. Answering both with one string meant comparing a stored + /// outputonly against the words Output Only, which never matched, so an + /// output-only cell kept an override it did not need. + /// + private static CellVisibilityState DefaultState( CellVisibilityHint hint, IReadOnlySet supported) { return hint switch { - CellVisibilityHint.Infrastructure => - supported.Contains(CellVisibilityState.Hidden) ? FormatStateName(CellVisibilityState.Hidden) : FormatStateName(CellVisibilityState.Visible), - CellVisibilityHint.OutputOnly => - supported.Contains(CellVisibilityState.OutputOnly) ? FormatStateName(CellVisibilityState.OutputOnly) : FormatStateName(CellVisibilityState.Visible), - _ => FormatStateName(CellVisibilityState.Visible), + CellVisibilityHint.Infrastructure when supported.Contains(CellVisibilityState.Hidden) => + CellVisibilityState.Hidden, + CellVisibilityHint.OutputOnly when supported.Contains(CellVisibilityState.OutputOnly) => + CellVisibilityState.OutputOnly, + _ => CellVisibilityState.Visible, }; } + // What a state is called on the wire and in cell metadata. Not a display name: this is + // stored in the notebook file and compared against, so it stays the same in every language. + private static string OptionValue(CellVisibilityState state) => state.ToString().ToLowerInvariant(); + private static string FormatStateName(CellVisibilityState state) => state switch { - CellVisibilityState.Visible => "Visible", - CellVisibilityState.Hidden => "Hidden", - CellVisibilityState.OutputOnly => "Output Only", - CellVisibilityState.Collapsed => "Collapsed", + CellVisibilityState.Visible => Strings.Visibility_Visible, + CellVisibilityState.Hidden => Strings.Visibility_Hidden, + CellVisibilityState.OutputOnly => Strings.Visibility_OutputOnly, + CellVisibilityState.Collapsed => Strings.Visibility_Collapsed, _ => state.ToString(), }; } diff --git a/src/Verso/Extensions/ExtensionHost.cs b/src/Verso/Extensions/ExtensionHost.cs index c98f2106..c3ee3074 100644 --- a/src/Verso/Extensions/ExtensionHost.cs +++ b/src/Verso/Extensions/ExtensionHost.cs @@ -4,6 +4,7 @@ using System.Reflection.PortableExecutable; using System.Text.RegularExpressions; using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions; @@ -612,7 +613,7 @@ public async Task LoadFromDirectoryAsync(string path) ArgumentNullException.ThrowIfNull(path); if (!Directory.Exists(path)) - throw new DirectoryNotFoundException($"Extension directory not found: {path}"); + throw new DirectoryNotFoundException(string.Format(Strings.Error_ExtensionDirectoryNotFound, path)); foreach (var dll in Directory.GetFiles(path, "*.dll")) { @@ -630,7 +631,7 @@ public async Task LoadFromAssemblyAsync(string path) ArgumentNullException.ThrowIfNull(path); if (!File.Exists(path)) - throw new FileNotFoundException($"Extension assembly not found: {path}", path); + throw new FileNotFoundException(string.Format(Strings.Error_ExtensionAssemblyNotFound, path), path); var loadContext = new ExtensionLoadContext(path); var assembly = loadContext.LoadFromAssemblyPath(Path.GetFullPath(path)); diff --git a/src/Verso/Extensions/Formatters/CollectionFormatter.cs b/src/Verso/Extensions/Formatters/CollectionFormatter.cs index 5421c7e5..4ceec065 100644 --- a/src/Verso/Extensions/Formatters/CollectionFormatter.cs +++ b/src/Verso/Extensions/Formatters/CollectionFormatter.cs @@ -1,5 +1,6 @@ using System.Collections; using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Formatters; @@ -12,10 +13,10 @@ public sealed class CollectionFormatter : IDataFormatter // --- IExtension --- public string ExtensionId => "verso.formatter.collection"; - public string Name => "Collection Formatter"; + public string Name => Strings.Formatter_Collection; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Formats collections as HTML tables."; + public string? Description => Strings.Formatter_Collection_Description; // --- IDataFormatter --- diff --git a/src/Verso/Extensions/Formatters/ExceptionFormatter.cs b/src/Verso/Extensions/Formatters/ExceptionFormatter.cs index fa9b256e..baa6d869 100644 --- a/src/Verso/Extensions/Formatters/ExceptionFormatter.cs +++ b/src/Verso/Extensions/Formatters/ExceptionFormatter.cs @@ -1,6 +1,7 @@ using System.Net; using System.Text; using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Formatters; @@ -13,10 +14,10 @@ public sealed class ExceptionFormatter : IDataFormatter // --- IExtension --- public string ExtensionId => "verso.formatter.exception"; - public string Name => "Exception Formatter"; + public string Name => Strings.Formatter_Exception; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Formats exceptions as structured HTML."; + public string? Description => Strings.Formatter_Exception_Description; // --- IDataFormatter --- diff --git a/src/Verso/Extensions/Formatters/HtmlFormatter.cs b/src/Verso/Extensions/Formatters/HtmlFormatter.cs index 1acb74b9..4ab0ae55 100644 --- a/src/Verso/Extensions/Formatters/HtmlFormatter.cs +++ b/src/Verso/Extensions/Formatters/HtmlFormatter.cs @@ -1,5 +1,6 @@ using System.Reflection; using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Formatters; @@ -12,10 +13,10 @@ public sealed class HtmlFormatter : IDataFormatter // --- IExtension --- public string ExtensionId => "verso.formatter.html"; - public string Name => "HTML Formatter"; + public string Name => Strings.Formatter_Html; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Formats objects with a ToHtml() method as HTML."; + public string? Description => Strings.Formatter_Html_Description; // --- IDataFormatter --- diff --git a/src/Verso/Extensions/Formatters/ImageFormatter.cs b/src/Verso/Extensions/Formatters/ImageFormatter.cs index 36e338a4..9be8e10e 100644 --- a/src/Verso/Extensions/Formatters/ImageFormatter.cs +++ b/src/Verso/Extensions/Formatters/ImageFormatter.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Formatters; @@ -11,10 +12,10 @@ public sealed class ImageFormatter : IDataFormatter // --- IExtension --- public string ExtensionId => "verso.formatter.image"; - public string Name => "Image Formatter"; + public string Name => Strings.Formatter_Image; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Formats byte arrays as inline base64 images."; + public string? Description => Strings.Formatter_Image_Description; // --- IDataFormatter --- diff --git a/src/Verso/Extensions/Formatters/ObjectFormatter.cs b/src/Verso/Extensions/Formatters/ObjectFormatter.cs index 3a3bbe6b..ad4647c5 100644 --- a/src/Verso/Extensions/Formatters/ObjectFormatter.cs +++ b/src/Verso/Extensions/Formatters/ObjectFormatter.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Formatters; @@ -20,10 +21,10 @@ public sealed class ObjectFormatter : IDataFormatter // --- IExtension --- public string ExtensionId => "verso.formatter.object"; - public string Name => "Object Formatter"; + public string Name => Strings.Formatter_Object; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Formats objects as HTML tables showing public properties and fields."; + public string? Description => Strings.Formatter_Object_Description; // --- IDataFormatter --- diff --git a/src/Verso/Extensions/Formatters/ObjectTreeRenderer.cs b/src/Verso/Extensions/Formatters/ObjectTreeRenderer.cs index 6ef6431a..197fd0ab 100644 --- a/src/Verso/Extensions/Formatters/ObjectTreeRenderer.cs +++ b/src/Verso/Extensions/Formatters/ObjectTreeRenderer.cs @@ -2,6 +2,8 @@ using System.Net; using System.Reflection; using System.Text; +using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Formatters; @@ -176,7 +178,10 @@ private static void RenderNestedObject(StringBuilder sb, object value, Type type sb.Append($"
    "); sb.Append(""); sb.Append($"
    {typeName}"); - sb.Append($" ({members.Length} {(members.Length == 1 ? "member" : "members")})"); + var memberCount = string.Format( + Plural.Of(members.Length, Strings.ObjectTree_MemberCount_One, Strings.ObjectTree_MemberCount_Other), + members.Length); + sb.Append($" {memberCount}"); sb.Append(""); sb.Append(""); @@ -201,7 +206,7 @@ private static void RenderNestedCollection(StringBuilder sb, IEnumerable enumera if (items.Count == 0) { - sb.Append("Empty collection"); + sb.Append($"{Strings.ObjectTree_EmptyCollection}"); return; } @@ -211,7 +216,7 @@ private static void RenderNestedCollection(StringBuilder sb, IEnumerable enumera var firstNonNull = items.FirstOrDefault(i => i is not null); if (firstNonNull is null) { - sb.Append("Empty collection"); + sb.Append($"{Strings.ObjectTree_EmptyCollection}"); return; } @@ -223,7 +228,8 @@ private static void RenderNestedCollection(StringBuilder sb, IEnumerable enumera sb.Append($"
    "); sb.Append(""); sb.Append($"{typeName}"); - sb.Append($" ({displayCount}{(truncated ? "+" : "")} items)"); + var itemCount = string.Format(Strings.ObjectTree_ItemCount, $"{displayCount}{(truncated ? "+" : "")}"); + sb.Append($" {itemCount}"); sb.Append(""); sb.Append("
    "); @@ -281,7 +287,7 @@ private static void RenderNestedCollection(StringBuilder sb, IEnumerable enumera sb.Append("
    "); if (truncated) - sb.Append($"
    Showing {displayCount} of more items
    "); + sb.Append($"
    {string.Format(Strings.ObjectTree_ShowingMore, displayCount)}
    "); sb.Append(""); } diff --git a/src/Verso/Extensions/Formatters/PrimitiveFormatter.cs b/src/Verso/Extensions/Formatters/PrimitiveFormatter.cs index c202f6ef..83f1647f 100644 --- a/src/Verso/Extensions/Formatters/PrimitiveFormatter.cs +++ b/src/Verso/Extensions/Formatters/PrimitiveFormatter.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Formatters; @@ -26,10 +27,10 @@ public sealed class PrimitiveFormatter : IDataFormatter // --- IExtension --- public string ExtensionId => "verso.formatter.primitive"; - public string Name => "Primitive Formatter"; + public string Name => Strings.Formatter_Primitive; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Formats primitive and common value types as plain text."; + public string? Description => Strings.Formatter_Primitive_Description; // --- IDataFormatter --- diff --git a/src/Verso/Extensions/Formatters/SvgFormatter.cs b/src/Verso/Extensions/Formatters/SvgFormatter.cs index 7b609000..83147beb 100644 --- a/src/Verso/Extensions/Formatters/SvgFormatter.cs +++ b/src/Verso/Extensions/Formatters/SvgFormatter.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Formatters; @@ -11,10 +12,10 @@ public sealed class SvgFormatter : IDataFormatter // --- IExtension --- public string ExtensionId => "verso.formatter.svg"; - public string Name => "SVG Formatter"; + public string Name => Strings.Formatter_Svg; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Formats SVG strings as inline HTML."; + public string? Description => Strings.Formatter_Svg_Description; // --- IDataFormatter --- diff --git a/src/Verso/Extensions/Kernels/HtmlKernel.cs b/src/Verso/Extensions/Kernels/HtmlKernel.cs index 3553ca72..be616ed8 100644 --- a/src/Verso/Extensions/Kernels/HtmlKernel.cs +++ b/src/Verso/Extensions/Kernels/HtmlKernel.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; using Verso.Abstractions; +using Verso.Resources; using Verso.Extensions.Utilities; namespace Verso.Extensions.Kernels; @@ -18,10 +19,10 @@ public sealed class HtmlKernel : ILanguageKernel // --- IExtension --- public string ExtensionId => "verso.kernel.html"; - public string Name => "HTML Kernel"; + public string Name => Strings.Kernel_Html; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Executes HTML cells with @variable substitution."; + public string? Description => Strings.Kernel_Html_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -29,6 +30,7 @@ public sealed class HtmlKernel : ILanguageKernel // --- ILanguageKernel --- public string LanguageId => "html"; + // Format and product names read the same in every language. public string DisplayName => "HTML"; public IReadOnlyList FileExtensions { get; } = new[] { ".html", ".htm" }; @@ -91,7 +93,7 @@ public Task> GetDiagnosticsAsync(string code) diagnostics.Add(new Diagnostic( DiagnosticSeverity.Warning, - $"Unresolved variable '@{name}'. No matching variable found in the variable store.", + string.Format(Strings.Kernel_UnresolvedVariable, name), startLine, startCol, endLine, endCol)); } diff --git a/src/Verso/Extensions/Kernels/MermaidKernel.cs b/src/Verso/Extensions/Kernels/MermaidKernel.cs index 10bb2a62..ece5380c 100644 --- a/src/Verso/Extensions/Kernels/MermaidKernel.cs +++ b/src/Verso/Extensions/Kernels/MermaidKernel.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; using Verso.Abstractions; +using Verso.Resources; using Verso.Extensions.Utilities; namespace Verso.Extensions.Kernels; @@ -16,10 +17,10 @@ public sealed class MermaidKernel : ILanguageKernel // --- IExtension --- public string ExtensionId => "verso.kernel.mermaid"; - public string Name => "Mermaid Kernel"; + public string Name => Strings.Kernel_Mermaid; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Executes Mermaid diagram cells with @variable substitution."; + public string? Description => Strings.Kernel_Mermaid_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -27,6 +28,7 @@ public sealed class MermaidKernel : ILanguageKernel // --- ILanguageKernel --- public string LanguageId => "mermaid"; + // Format and product names read the same in every language. public string DisplayName => "Mermaid"; public IReadOnlyList FileExtensions { get; } = new[] { ".mmd", ".mermaid" }; @@ -99,7 +101,7 @@ public Task> GetDiagnosticsAsync(string code) diagnostics.Add(new Diagnostic( DiagnosticSeverity.Warning, - $"Unresolved variable '@{name}'. No matching variable found in the variable store.", + string.Format(Strings.Kernel_UnresolvedVariable, name), startLine, startCol, endLine, endCol)); } diff --git a/src/Verso/Extensions/Layouts/ContentFallbackRenderer.cs b/src/Verso/Extensions/Layouts/ContentFallbackRenderer.cs index 31710149..10afdf8b 100644 --- a/src/Verso/Extensions/Layouts/ContentFallbackRenderer.cs +++ b/src/Verso/Extensions/Layouts/ContentFallbackRenderer.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Layouts; @@ -10,13 +11,13 @@ namespace Verso.Extensions.Layouts; internal sealed class ContentFallbackRenderer : ICellRenderer { public string ExtensionId => "verso.internal.fallback-renderer"; - public string Name => "Fallback Renderer"; + public string Name => Strings.Renderer_Fallback; public string Version => "1.0.0"; public string? Author => null; public string? Description => null; public string CellTypeId => ""; - public string DisplayName => "Fallback"; + public string DisplayName => Strings.Renderer_Fallback_Label; public bool CollapsesInputOnExecute => false; public CellVisibilityHint DefaultVisibility => CellVisibilityHint.Content; diff --git a/src/Verso/Extensions/Layouts/DashboardLayout.cs b/src/Verso/Extensions/Layouts/DashboardLayout.cs index d027adbc..7056a51e 100644 --- a/src/Verso/Extensions/Layouts/DashboardLayout.cs +++ b/src/Verso/Extensions/Layouts/DashboardLayout.cs @@ -1,6 +1,7 @@ using System.Text; using System.Text.Json; using Verso.Abstractions; +using Verso.Resources; using Verso.Extensions.Utilities; namespace Verso.Extensions.Layouts; @@ -21,15 +22,15 @@ public sealed class DashboardLayout : ILayoutEngine, ILayoutInteractionHandler // --- IExtension --- public string ExtensionId => "verso.layout.dashboard"; - public string Name => "Dashboard Layout"; + public string Name => Strings.Layout_Dashboard; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Grid-based dashboard layout showing output-only cells."; + public string? Description => Strings.Layout_Dashboard_Description; // --- ILayoutEngine --- public string LayoutId => "dashboard"; - public string DisplayName => "Dashboard"; + public string DisplayName => Strings.Layout_Dashboard_Label; public string? Icon => null; public bool RequiresCustomRenderer => true; @@ -98,11 +99,14 @@ public Task RenderLayoutAsync(IReadOnlyList cells, IVer // Toolbar IS the drag handle: clicking empty space drags, clicking the Run button // fires its data-action via layout-interact. dashboard-interop.js skips drag when the // mousedown target is inside a ") - .Append("") + .Append("\" title=\"").Append(System.Net.WebUtility.HtmlEncode(Strings.Layout_RunCell)) + .Append("\">") + .Append("") .Append(""); // Resize handle (bottom-right corner). diff --git a/src/Verso/Extensions/Layouts/NotebookLayout.cs b/src/Verso/Extensions/Layouts/NotebookLayout.cs index 28727af3..f8caccee 100644 --- a/src/Verso/Extensions/Layouts/NotebookLayout.cs +++ b/src/Verso/Extensions/Layouts/NotebookLayout.cs @@ -1,5 +1,6 @@ using System.Text; using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Layouts; @@ -23,10 +24,10 @@ public sealed class NotebookLayout : ILayoutEngine, ILayoutInteractionHandler // --- IExtension --- public string ExtensionId => "verso.layout.notebook"; - public string Name => "Notebook Layout"; + public string Name => Strings.Layout_Notebook; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Linear top-to-bottom notebook layout with live, editable cells in elevated cards."; + public string? Description => Strings.Layout_Notebook_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -34,7 +35,7 @@ public sealed class NotebookLayout : ILayoutEngine, ILayoutInteractionHandler // --- ILayoutEngine --- public string LayoutId => "notebook"; - public string DisplayName => "Notebook"; + public string DisplayName => Strings.Layout_Notebook_Label; public string? Icon => null; public bool RequiresCustomRenderer => true; @@ -107,8 +108,8 @@ public Task RenderLayoutAsync(IReadOnlyList cells, IVer if (cells.Count == 0) { sb.Append("
    ") - .Append("

    This notebook is empty

    ") - .Append("

    Add your first cell below.

    ") + .Append("

    ").Append(Escape(Strings.Layout_EmptyTitle)).Append("

    ") + .Append("

    ").Append(Escape(Strings.Layout_EmptySubtitle)).Append("

    ") .Append("
    "); } @@ -160,7 +161,8 @@ public Task RenderLayoutAsync(IReadOnlyList cells, IVer .Append(cells.Count) .Append("\" data-type=\"").Append(ct.Id).Append("\">") .Append("+ ") - .Append(ct.DisplayName).Append(" Cell"); + .Append(Escape(string.Format(Strings.Layout_AddCell, ct.DisplayName))) + .Append(""); } sb.Append(""); @@ -214,6 +216,11 @@ public async Task OnLayoutInteractionAsync(LayoutInteractionContext context) // --- HTML helpers --- + // Text drawn into the layout's own markup passes through here first. Cell type names reach + // this file from extensions, and the words around them from a resource file, so neither is + // guaranteed to be free of characters that would otherwise close a tag or an attribute. + private static string Escape(string value) => System.Net.WebUtility.HtmlEncode(value); + private static void AppendInsertRail(StringBuilder sb, int index, IReadOnlyList cellTypes) { sb.Append("
    "); @@ -222,9 +229,11 @@ private static void AppendInsertRail(StringBuilder sb, int index, IReadOnlyList< sb.Append(""); + .Append(Escape(ct.DisplayName)).Append(""); } sb.Append("
    "); } @@ -236,7 +245,9 @@ private static void AppendInsertRail(StringBuilder sb, int index, IReadOnlyList< /// private static IReadOnlyList GetAvailableCellTypes(IVersoContext context) { - var types = new List { new("code", "Code") }; + // Code has no ICellType of its own: it is what a cell is when nothing else claims it, + // so the engine has to name it here rather than reading the name off a registration. + var types = new List { new("code", Strings.CellType_Code) }; var host = context.ExtensionHost; var registeredTypes = host.GetCellTypes(); diff --git a/src/Verso/Extensions/Layouts/PresentationLayout.cs b/src/Verso/Extensions/Layouts/PresentationLayout.cs index c0bbef27..20bbe66d 100644 --- a/src/Verso/Extensions/Layouts/PresentationLayout.cs +++ b/src/Verso/Extensions/Layouts/PresentationLayout.cs @@ -1,6 +1,7 @@ using System.Net; using System.Text; using Verso.Abstractions; +using Verso.Resources; using Verso.Extensions.Utilities; namespace Verso.Extensions.Layouts; @@ -16,15 +17,15 @@ public sealed class PresentationLayout : ILayoutEngine // --- IExtension --- public string ExtensionId => "verso.layout.presentation"; - public string Name => "Presentation Layout"; + public string Name => Strings.Layout_Presentation; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Output-only presentation layout for consuming interactive notebooks."; + public string? Description => Strings.Layout_Presentation_Description; // --- ILayoutEngine --- public string LayoutId => "presentation"; - public string DisplayName => "Presentation"; + public string DisplayName => Strings.Layout_Presentation_Label; public string? Icon => null; public LayoutCapabilities Capabilities => LayoutCapabilities.None; diff --git a/src/Verso/Extensions/Marketplace/MarketplaceLoader.cs b/src/Verso/Extensions/Marketplace/MarketplaceLoader.cs index 23186a89..eb880395 100644 --- a/src/Verso/Extensions/Marketplace/MarketplaceLoader.cs +++ b/src/Verso/Extensions/Marketplace/MarketplaceLoader.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Marketplace; @@ -142,7 +143,7 @@ public static async Task LoadRequiredAsync( { var needConsent = plans .Where(p => p.EffectiveVersion is not null && !trustStore.IsApproved(p.Id, p.EffectiveVersion)) - .Select(p => new ExtensionConsentInfo(p.Id, p.EffectiveVersion, "notebook required extensions")) + .Select(p => new ExtensionConsentInfo(p.Id, p.EffectiveVersion, Strings.Consent_Source_RequiredExtensions)) .ToList(); if (needConsent.Count > 0) @@ -278,7 +279,8 @@ public static async Task InstallLocalFileAsync( if (!trustStore.IsApproved(id, version)) { - var consent = new[] { new ExtensionConsentInfo(id, version, $"local file: {Path.GetFileName(localFilePath)}") }; + var consent = new[] { new ExtensionConsentInfo(id, version, + string.Format(Strings.Consent_Source_LocalFile, Path.GetFileName(localFilePath))) }; var approved = await extensionHost.RequestExtensionConsentAsync(consent, ct); if (!approved) return new LocalInstallOutcome(false, id, version, "Installation was not approved.", 0); diff --git a/src/Verso/Extensions/Renderers/HtmlCellRenderer.cs b/src/Verso/Extensions/Renderers/HtmlCellRenderer.cs index b7f01848..a95ae157 100644 --- a/src/Verso/Extensions/Renderers/HtmlCellRenderer.cs +++ b/src/Verso/Extensions/Renderers/HtmlCellRenderer.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Renderers; @@ -11,14 +12,15 @@ public sealed class HtmlCellRenderer : ICellRenderer // --- IExtension --- public string ExtensionId => "verso.renderer.html"; - public string Name => "HTML Renderer"; + public string Name => Strings.Renderer_Html; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Renders HTML cells with input collapse on execute."; + public string? Description => Strings.Renderer_Html_Description; // --- ICellRenderer --- public string CellTypeId => "html"; + // Format and product names read the same in every language. public string DisplayName => "HTML"; public bool CollapsesInputOnExecute => true; diff --git a/src/Verso/Extensions/Renderers/MarkdownRenderer.cs b/src/Verso/Extensions/Renderers/MarkdownRenderer.cs index 35a5ea05..b9d31f84 100644 --- a/src/Verso/Extensions/Renderers/MarkdownRenderer.cs +++ b/src/Verso/Extensions/Renderers/MarkdownRenderer.cs @@ -1,5 +1,6 @@ using Markdig; using Verso.Abstractions; +using Verso.Resources; using Verso.Extensions.Utilities; namespace Verso.Extensions.Renderers; @@ -17,14 +18,15 @@ public sealed class MarkdownRenderer : ICellRenderer // --- IExtension --- public string ExtensionId => "verso.renderer.markdown"; - public string Name => "Markdown Renderer"; + public string Name => Strings.Renderer_Markdown; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Renders Markdown cells using Markdig."; + public string? Description => Strings.Renderer_Markdown_Description; // --- ICellRenderer --- public string CellTypeId => "markdown"; + // Format and product names read the same in every language. public string DisplayName => "Markdown"; public bool CollapsesInputOnExecute => true; diff --git a/src/Verso/Extensions/Renderers/MermaidCellRenderer.cs b/src/Verso/Extensions/Renderers/MermaidCellRenderer.cs index 83ba177c..ba206d9c 100644 --- a/src/Verso/Extensions/Renderers/MermaidCellRenderer.cs +++ b/src/Verso/Extensions/Renderers/MermaidCellRenderer.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Renderers; @@ -11,14 +12,15 @@ public sealed class MermaidCellRenderer : ICellRenderer // --- IExtension --- public string ExtensionId => "verso.renderer.mermaid"; - public string Name => "Mermaid Renderer"; + public string Name => Strings.Renderer_Mermaid; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Renders Mermaid diagram cells with input collapse on execute."; + public string? Description => Strings.Renderer_Mermaid_Description; // --- ICellRenderer --- public string CellTypeId => "mermaid"; + // Format and product names read the same in every language. public string DisplayName => "Mermaid"; public bool CollapsesInputOnExecute => true; diff --git a/src/Verso/Extensions/Renderers/ParametersCellRenderer.cs b/src/Verso/Extensions/Renderers/ParametersCellRenderer.cs index 60a61f61..11f09880 100644 --- a/src/Verso/Extensions/Renderers/ParametersCellRenderer.cs +++ b/src/Verso/Extensions/Renderers/ParametersCellRenderer.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.Json; using Verso.Abstractions; +using Verso.Resources; using Verso.Parameters; namespace Verso.Extensions.Renderers; @@ -18,13 +19,13 @@ public sealed class ParametersCellRenderer : ICellRenderer, ICellInteractionHand PropertyNameCaseInsensitive = true }; public string ExtensionId => "verso.renderer.parameters"; - public string Name => "Parameters Renderer"; + public string Name => Strings.Renderer_Parameters; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Renders parameter definitions as an interactive form with type-aware inputs."; + public string? Description => Strings.Renderer_Parameters_Description; public string CellTypeId => "parameters"; - public string DisplayName => "Parameters"; + public string DisplayName => Strings.CellType_Parameters_Label; public bool CollapsesInputOnExecute => false; public CellVisibilityHint DefaultVisibility => CellVisibilityHint.Infrastructure; @@ -59,6 +60,12 @@ public Task RenderOutputAsync(CellOutput output, ICellRenderContex } // --- Interaction handlers --- + // + // The messages naming a malformed payload stay in English on purpose. They can only appear + // when the front end sends something this renderer cannot read, which is a fault in the + // code rather than anything the reader did, and the wording is what somebody searches for + // when they report it. Everything a reader can actually cause, such as a value that does + // not fit its type, is translated. private static string HandleParameterUpdate(CellInteractionContext context) { @@ -68,10 +75,10 @@ private static string HandleParameterUpdate(CellInteractionContext context) var parameters = context.NotebookModel?.Parameters; if (parameters is null || !parameters.TryGetValue(payload.Name, out var def)) - return RenderError($"Parameter '{payload.Name}' not found."); + return RenderError(string.Format(Strings.Parameters_NotFound, payload.Name)); if (!ParameterValueParser.TryParse(def.Type, payload.Value ?? "", out var typed, out var error)) - return RenderErrorForField(payload.Name, error ?? "Invalid value."); + return RenderErrorForField(payload.Name, error ?? Strings.Parameters_InvalidValue); def.Default = typed; context.Variables?.Set(payload.Name, typed!); @@ -130,7 +137,7 @@ private static string HandleParameterRemove(CellInteractionContext context) var parameters = context.NotebookModel?.Parameters; if (parameters is null) - return RenderError("No parameters defined."); + return RenderError(Strings.Parameters_None); parameters.Remove(payload.Name); context.Variables?.Remove(payload.Name); @@ -147,7 +154,7 @@ private static string HandleParameterSubmit(CellInteractionContext context) var parameters = context.NotebookModel?.Parameters; if (parameters is null) - return RenderError("No parameters defined."); + return RenderError(Strings.Parameters_None); var errors = new Dictionary(); var parsed = new Dictionary(); @@ -163,7 +170,7 @@ private static string HandleParameterSubmit(CellInteractionContext context) if (!ParameterValueParser.TryParse(def.Type, value, out var typed, out var error)) { - errors[name] = error ?? "Invalid value."; + errors[name] = error ?? Strings.Parameters_InvalidValue; continue; } @@ -190,7 +197,7 @@ private static string HandleToggleRequired(CellInteractionContext context) var parameters = context.NotebookModel?.Parameters; if (parameters is null || !parameters.TryGetValue(payload.Name, out var def)) - return RenderError($"Parameter '{payload.Name}' not found."); + return RenderError(string.Format(Strings.Parameters_NotFound, payload.Name)); def.Required = string.Equals(payload.Value, "true", StringComparison.OrdinalIgnoreCase); context.StateChanged = true; @@ -233,7 +240,7 @@ private static string RenderEmptyState() { var sb = new StringBuilder(); sb.Append("
    "); - sb.Append("

    No parameters defined.

    "); + sb.Append("

    ").Append(Encode(Strings.Parameters_None)).Append("

    "); // Hidden inline form (same structure as RenderExpandedForm) so the // JS handler for data-action="parameter-add" can reveal it. @@ -241,7 +248,8 @@ private static string RenderEmptyState() sb.Append(""); sb.Append(""); sb.Append(""); - sb.Append(""); + sb.Append(""); sb.Append(""); sb.Append(""); sb.Append(""); sb.Append(""); sb.Append(""); - sb.Append(""); + sb.Append(""); sb.Append(""); sb.Append(""); - sb.Append(""); + sb.Append(""); sb.Append(""); sb.Append(""); - sb.Append(""); + sb.Append(""); sb.Append(""); sb.Append(""); - sb.Append(""); - sb.Append(""); + sb.Append(""); + sb.Append(""); sb.Append(""); sb.Append(""); sb.Append(""); - sb.Append(""); + sb.Append(""); sb.Append("
    "); return sb.ToString(); } @@ -283,7 +297,7 @@ private static string RenderExpandedForm( var sb = new StringBuilder(); sb.Append("
    "); sb.Append("
    "); - sb.Append("Parameters"); + sb.Append("").Append(Encode(Strings.Parameters_Title)).Append(""); sb.Append("("); sb.Append(sorted.Count); sb.Append(")"); @@ -291,12 +305,17 @@ private static string RenderExpandedForm( if (submitted) { - sb.Append("
    Parameters applied successfully.
    "); + sb.Append("
    ").Append(Encode(Strings.Parameters_Applied)).Append("
    "); } sb.Append(""); sb.Append(""); - sb.Append(""); + sb.Append(""); sb.Append(""); sb.Append(""); @@ -348,7 +367,7 @@ private static string RenderExpandedForm( // Remove button sb.Append(""); + sb.Append("\" title=\"").Append(Encode(Strings.Parameters_Remove)).Append("\">✕"); sb.Append(""); } @@ -356,7 +375,8 @@ private static string RenderExpandedForm( // Inline "add parameter" row (hidden by default, shown via JS) sb.Append(""); sb.Append(""); sb.Append(""); sb.Append(""); sb.Append(""); sb.Append(""); sb.Append(""); sb.Append(""); sb.Append("
    NameTypeDescriptionDefaultRequired").Append(Encode(Strings.Parameters_ColumnName)) + .Append("").Append(Encode(Strings.Parameters_ColumnType)) + .Append("").Append(Encode(Strings.Parameters_ColumnDescription)) + .Append("").Append(Encode(Strings.Parameters_ColumnDefault)) + .Append("").Append(Encode(Strings.Parameters_ColumnRequired)) + .Append("
    "); - sb.Append(""); + sb.Append(""); sb.Append(""); sb.Append(""); sb.Append(""); - sb.Append(""); + sb.Append(""); sb.Append(""); - sb.Append(""); + sb.Append(""); sb.Append(""); - sb.Append(""); + sb.Append(""); sb.Append(""); - sb.Append(""); - sb.Append(""); + sb.Append(""); + sb.Append(""); sb.Append("
    "); sb.Append("
    "); - sb.Append(""); + sb.Append(""); sb.Append("
    "); sb.Append("
    "); diff --git a/src/Verso/Extensions/Themes/VersoDarkTheme.cs b/src/Verso/Extensions/Themes/VersoDarkTheme.cs index 508f004d..bc47974d 100644 --- a/src/Verso/Extensions/Themes/VersoDarkTheme.cs +++ b/src/Verso/Extensions/Themes/VersoDarkTheme.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Themes; @@ -11,15 +12,15 @@ public sealed class VersoDarkTheme : ITheme // --- IExtension --- public string ExtensionId => "verso.theme.dark"; - public string Name => "Verso Dark"; + public string Name => Strings.Theme_Dark; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Default dark theme for Verso notebooks."; + public string? Description => Strings.Theme_Dark_Description; // --- ITheme --- public string ThemeId => "verso-dark"; - public string DisplayName => "Verso Dark"; + public string DisplayName => Strings.Theme_Dark; public ThemeKind ThemeKind => ThemeKind.Dark; public ThemeColorTokens Colors { get; } = new ThemeColorTokens diff --git a/src/Verso/Extensions/Themes/VersoHighContrastTheme.cs b/src/Verso/Extensions/Themes/VersoHighContrastTheme.cs index ff8b044c..8f1a93be 100644 --- a/src/Verso/Extensions/Themes/VersoHighContrastTheme.cs +++ b/src/Verso/Extensions/Themes/VersoHighContrastTheme.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Themes; @@ -12,15 +13,15 @@ public sealed class VersoHighContrastTheme : ITheme // --- IExtension --- public string ExtensionId => "verso.theme.highcontrast"; - public string Name => "Verso High Contrast"; + public string Name => Strings.Theme_HighContrast; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "High-contrast accessibility theme with WCAG 2.1 AA compliant color tokens."; + public string? Description => Strings.Theme_HighContrast_Description; // --- ITheme --- public string ThemeId => "verso-highcontrast"; - public string DisplayName => "Verso High Contrast"; + public string DisplayName => Strings.Theme_HighContrast; public ThemeKind ThemeKind => ThemeKind.HighContrast; public ThemeColorTokens Colors { get; } = new ThemeColorTokens diff --git a/src/Verso/Extensions/Themes/VersoLightTheme.cs b/src/Verso/Extensions/Themes/VersoLightTheme.cs index bcb06a2c..8e152b0c 100644 --- a/src/Verso/Extensions/Themes/VersoLightTheme.cs +++ b/src/Verso/Extensions/Themes/VersoLightTheme.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.Themes; @@ -11,15 +12,15 @@ public sealed class VersoLightTheme : ITheme // --- IExtension --- public string ExtensionId => "verso.theme.light"; - public string Name => "Verso Light"; + public string Name => Strings.Theme_Light; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Default light theme for Verso notebooks."; + public string? Description => Strings.Theme_Light_Description; // --- ITheme --- public string ThemeId => "verso-light"; - public string DisplayName => "Verso Light"; + public string DisplayName => Strings.Theme_Light; public ThemeKind ThemeKind => ThemeKind.Light; public ThemeColorTokens Colors { get; } = new(); diff --git a/src/Verso/Extensions/ToolbarActions/ClearCellOutputAction.cs b/src/Verso/Extensions/ToolbarActions/ClearCellOutputAction.cs index 05260387..41579ecb 100644 --- a/src/Verso/Extensions/ToolbarActions/ClearCellOutputAction.cs +++ b/src/Verso/Extensions/ToolbarActions/ClearCellOutputAction.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.ToolbarActions; @@ -6,16 +7,16 @@ namespace Verso.Extensions.ToolbarActions; public sealed class ClearCellOutputAction : IToolbarAction { public string ExtensionId => "verso.action.clear-cell-output"; - public string Name => "Clear Cell Output"; + public string Name => Strings.Action_ClearCellOutput; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Clears the output of the selected cell."; + public string? Description => Strings.Action_ClearCellOutput_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; public string ActionId => "verso.action.clear-cell-output"; - public string DisplayName => "Clear Output"; + public string DisplayName => Strings.Action_ClearCellOutput_Label; public string? Icon => ""; public ToolbarPlacement Placement => ToolbarPlacement.CellToolbar; public int Order => 31; diff --git a/src/Verso/Extensions/ToolbarActions/ClearOutputsAction.cs b/src/Verso/Extensions/ToolbarActions/ClearOutputsAction.cs index 9118e4c7..e32ef37d 100644 --- a/src/Verso/Extensions/ToolbarActions/ClearOutputsAction.cs +++ b/src/Verso/Extensions/ToolbarActions/ClearOutputsAction.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.ToolbarActions; @@ -11,10 +12,10 @@ public sealed class ClearOutputsAction : IToolbarAction // --- IExtension --- public string ExtensionId => "verso.action.clear-outputs"; - public string Name => "Clear Outputs"; + public string Name => Strings.Action_ClearOutputs; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Clears all cell outputs in the notebook."; + public string? Description => Strings.Action_ClearOutputs_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -22,7 +23,7 @@ public sealed class ClearOutputsAction : IToolbarAction // --- IToolbarAction --- public string ActionId => "verso.action.clear-outputs"; - public string DisplayName => "Clear Outputs"; + public string DisplayName => Strings.Action_ClearOutputs; public string? Icon => ""; public bool IconOnly => true; public ToolbarPlacement Placement => ToolbarPlacement.MainToolbar; diff --git a/src/Verso/Extensions/ToolbarActions/ExportHtmlAction.cs b/src/Verso/Extensions/ToolbarActions/ExportHtmlAction.cs index 2623fd1e..9cd14c86 100644 --- a/src/Verso/Extensions/ToolbarActions/ExportHtmlAction.cs +++ b/src/Verso/Extensions/ToolbarActions/ExportHtmlAction.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; using Verso.Export; namespace Verso.Extensions.ToolbarActions; @@ -12,10 +13,10 @@ public sealed class ExportHtmlAction : IToolbarAction // --- IExtension --- public string ExtensionId => "verso.action.export-html"; - public string Name => "Export HTML"; + public string Name => Strings.Action_ExportHtml; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Exports the notebook as a self-contained HTML document."; + public string? Description => Strings.Action_ExportHtml_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -23,6 +24,7 @@ public sealed class ExportHtmlAction : IToolbarAction // --- IToolbarAction --- public string ActionId => "verso.action.export-html"; + // The export menu names formats, and a format name reads the same in every language. public string DisplayName => "HTML"; public string? Icon => null; public ToolbarPlacement Placement => ToolbarPlacement.ExportMenu; diff --git a/src/Verso/Extensions/ToolbarActions/ExportMarkdownAction.cs b/src/Verso/Extensions/ToolbarActions/ExportMarkdownAction.cs index a519c034..b7563c86 100644 --- a/src/Verso/Extensions/ToolbarActions/ExportMarkdownAction.cs +++ b/src/Verso/Extensions/ToolbarActions/ExportMarkdownAction.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; using Verso.Export; namespace Verso.Extensions.ToolbarActions; @@ -12,10 +13,10 @@ public sealed class ExportMarkdownAction : IToolbarAction // --- IExtension --- public string ExtensionId => "verso.action.export-markdown"; - public string Name => "Export Markdown"; + public string Name => Strings.Action_ExportMarkdown; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Exports the notebook as a Markdown document."; + public string? Description => Strings.Action_ExportMarkdown_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -23,6 +24,7 @@ public sealed class ExportMarkdownAction : IToolbarAction // --- IToolbarAction --- public string ActionId => "verso.action.export-markdown"; + // The export menu names formats, and a format name reads the same in every language. public string DisplayName => "Markdown"; public string? Icon => null; public ToolbarPlacement Placement => ToolbarPlacement.ExportMenu; diff --git a/src/Verso/Extensions/ToolbarActions/ExportVersoAction.cs b/src/Verso/Extensions/ToolbarActions/ExportVersoAction.cs index 3a3ae30c..c4030900 100644 --- a/src/Verso/Extensions/ToolbarActions/ExportVersoAction.cs +++ b/src/Verso/Extensions/ToolbarActions/ExportVersoAction.cs @@ -1,5 +1,6 @@ using System.Text; using Verso.Abstractions; +using Verso.Resources; using Verso.Serializers; namespace Verso.Extensions.ToolbarActions; @@ -16,10 +17,10 @@ public sealed class ExportVersoAction : IToolbarAction // --- IExtension --- public string ExtensionId => "verso.action.export-verso"; - public string Name => "Export Verso"; + public string Name => Strings.Action_ExportVerso; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Exports a Markdown-backed notebook as a native .verso file."; + public string? Description => Strings.Action_ExportVerso_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -27,6 +28,7 @@ public sealed class ExportVersoAction : IToolbarAction // --- IToolbarAction --- public string ActionId => "verso.action.export-verso"; + // The export menu names formats, and this one is the product's own. public string DisplayName => "Verso"; public string? Icon => null; public ToolbarPlacement Placement => ToolbarPlacement.ExportMenu; diff --git a/src/Verso/Extensions/ToolbarActions/RestartKernelAction.cs b/src/Verso/Extensions/ToolbarActions/RestartKernelAction.cs index 6fc6cbed..321afc43 100644 --- a/src/Verso/Extensions/ToolbarActions/RestartKernelAction.cs +++ b/src/Verso/Extensions/ToolbarActions/RestartKernelAction.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.ToolbarActions; @@ -11,10 +12,10 @@ public sealed class RestartKernelAction : IToolbarAction // --- IExtension --- public string ExtensionId => "verso.action.restart-kernel"; - public string Name => "Restart Kernel"; + public string Name => Strings.Action_RestartKernel; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Restarts the active language kernel."; + public string? Description => Strings.Action_RestartKernel_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -22,11 +23,10 @@ public sealed class RestartKernelAction : IToolbarAction // --- IToolbarAction --- public string ActionId => "verso.action.restart-kernel"; - public string DisplayName => "Restart Kernel"; + public string DisplayName => Strings.Action_RestartKernel; public string? Icon => ""; public bool IconOnly => true; - public string? ConfirmationPrompt => - "Restarting the kernel discards all variables and execution state. Restart now?"; + public string? ConfirmationPrompt => Strings.Action_RestartKernel_Confirm; public ToolbarPlacement Placement => ToolbarPlacement.MainToolbar; public int Order => 40; diff --git a/src/Verso/Extensions/ToolbarActions/RunAllAction.cs b/src/Verso/Extensions/ToolbarActions/RunAllAction.cs index efc8570a..a20115bd 100644 --- a/src/Verso/Extensions/ToolbarActions/RunAllAction.cs +++ b/src/Verso/Extensions/ToolbarActions/RunAllAction.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.ToolbarActions; @@ -11,10 +12,10 @@ public sealed class RunAllAction : IToolbarAction // --- IExtension --- public string ExtensionId => "verso.action.run-all"; - public string Name => "Run All"; + public string Name => Strings.Action_RunAll; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Executes all cells in the notebook."; + public string? Description => Strings.Action_RunAll_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -22,7 +23,7 @@ public sealed class RunAllAction : IToolbarAction // --- IToolbarAction --- public string ActionId => "verso.action.run-all"; - public string DisplayName => "Run All"; + public string DisplayName => Strings.Action_RunAll; public string? Icon => ""; public bool IsPrimary => true; public ToolbarPlacement Placement => ToolbarPlacement.MainToolbar; diff --git a/src/Verso/Extensions/ToolbarActions/RunCellAction.cs b/src/Verso/Extensions/ToolbarActions/RunCellAction.cs index 48433fd2..f33bcf08 100644 --- a/src/Verso/Extensions/ToolbarActions/RunCellAction.cs +++ b/src/Verso/Extensions/ToolbarActions/RunCellAction.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.ToolbarActions; @@ -11,10 +12,10 @@ public sealed class RunCellAction : IToolbarAction // --- IExtension --- public string ExtensionId => "verso.action.run-cell"; - public string Name => "Run Cell"; + public string Name => Strings.Action_RunCell; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Executes the selected cell(s)."; + public string? Description => Strings.Action_RunCell_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -22,7 +23,7 @@ public sealed class RunCellAction : IToolbarAction // --- IToolbarAction --- public string ActionId => "verso.action.run-cell"; - public string DisplayName => "Run Cell"; + public string DisplayName => Strings.Action_RunCell; public string? Icon => null; public ToolbarPlacement Placement => ToolbarPlacement.CellToolbar; public int Order => 20; diff --git a/src/Verso/Extensions/ToolbarActions/SwitchLayoutAction.cs b/src/Verso/Extensions/ToolbarActions/SwitchLayoutAction.cs index 9af97c15..3ff272dd 100644 --- a/src/Verso/Extensions/ToolbarActions/SwitchLayoutAction.cs +++ b/src/Verso/Extensions/ToolbarActions/SwitchLayoutAction.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.ToolbarActions; @@ -11,10 +12,10 @@ public sealed class SwitchLayoutAction : IToolbarAction // --- IExtension --- public string ExtensionId => "verso.action.switch-layout"; - public string Name => "Switch Layout"; + public string Name => Strings.Action_SwitchLayout; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Cycles between available layout engines."; + public string? Description => Strings.Action_SwitchLayout_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -22,7 +23,7 @@ public sealed class SwitchLayoutAction : IToolbarAction // --- IToolbarAction --- public string ActionId => "verso.switchLayout"; - public string DisplayName => "Switch Layout"; + public string DisplayName => Strings.Action_SwitchLayout; public string? Icon => null; public ToolbarPlacement Placement => ToolbarPlacement.MainToolbar; public int Order => 50; diff --git a/src/Verso/Extensions/ToolbarActions/SwitchThemeAction.cs b/src/Verso/Extensions/ToolbarActions/SwitchThemeAction.cs index f13f6ebb..7cb523a1 100644 --- a/src/Verso/Extensions/ToolbarActions/SwitchThemeAction.cs +++ b/src/Verso/Extensions/ToolbarActions/SwitchThemeAction.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.Extensions.ToolbarActions; @@ -11,10 +12,10 @@ public sealed class SwitchThemeAction : IToolbarAction // --- IExtension --- public string ExtensionId => "verso.action.switch-theme"; - public string Name => "Switch Theme"; + public string Name => Strings.Action_SwitchTheme; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Cycles between available themes."; + public string? Description => Strings.Action_SwitchTheme_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -22,7 +23,7 @@ public sealed class SwitchThemeAction : IToolbarAction // --- IToolbarAction --- public string ActionId => "verso.switchTheme"; - public string DisplayName => "Switch Theme"; + public string DisplayName => Strings.Action_SwitchTheme; public string? Icon => null; public ToolbarPlacement Placement => ToolbarPlacement.MainToolbar; public int Order => 55; diff --git a/src/Verso/Kernels/CSharpKernel.cs b/src/Verso/Kernels/CSharpKernel.cs index 79992d47..c53d586b 100644 --- a/src/Verso/Kernels/CSharpKernel.cs +++ b/src/Verso/Kernels/CSharpKernel.cs @@ -5,6 +5,7 @@ using Verso.MagicCommands; using VersoDiagnostic = Verso.Abstractions.Diagnostic; +using Verso.Resources; namespace Verso.Kernels; @@ -68,7 +69,7 @@ public CSharpKernel(CSharpKernelOptions options) public string Name => "C# (Roslyn)"; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "C# language kernel powered by Roslyn scripting."; + public string? Description => Strings.Kernel_CSharp_Description; // --- ILanguageKernel --- @@ -594,7 +595,7 @@ private static string FormatInstalledPackagesHtml(List packa var topItems = string.Join("", packages.Select(p => $"
  • {p.PackageId}, {p.ResolvedVersion}
  • ")); - var html = $"
    Installed Packages
      {topItems}
    "; + var html = $"
    {Strings.Kernel_InstalledPackages}
      {topItems}
    "; if (transitive.Count > 0) { diff --git a/src/Verso/Kernels/NuGetPackageResolver.cs b/src/Verso/Kernels/NuGetPackageResolver.cs index 4883dc0c..e22b76b3 100644 --- a/src/Verso/Kernels/NuGetPackageResolver.cs +++ b/src/Verso/Kernels/NuGetPackageResolver.cs @@ -6,6 +6,7 @@ using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Versioning; +using Verso.Resources; namespace Verso.Kernels; @@ -362,7 +363,8 @@ await ResolveWithDependenciesAsync( packageId, resolvedVersion, fileStream, cache, logger, ct).ConfigureAwait(false); if (!downloaded) - throw new InvalidOperationException($"Failed to download package '{packageId}' v{resolvedVersion}."); + throw new InvalidOperationException( + string.Format(Strings.Error_PackageDownloadFailed, packageId, resolvedVersion)); } var assemblyPaths = new List(); diff --git a/src/Verso/LayoutManager.cs b/src/Verso/LayoutManager.cs index 5a427df7..a750328d 100644 --- a/src/Verso/LayoutManager.cs +++ b/src/Verso/LayoutManager.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso; @@ -129,7 +130,7 @@ public void SetActiveLayout(string layoutId) public void SetActiveLayout(LayoutReference reference) { if (!TryActivate(reference)) - throw new InvalidOperationException($"Layout '{reference}' not found."); + throw new InvalidOperationException(string.Format(Strings.Error_LayoutNotFound, reference)); } /// diff --git a/src/Verso/Localization/CellText.cs b/src/Verso/Localization/CellText.cs new file mode 100644 index 00000000..135d3880 --- /dev/null +++ b/src/Verso/Localization/CellText.cs @@ -0,0 +1,20 @@ +using Verso.Resources; + +namespace Verso.Localization; + +/// +/// Puts the standard prefix in front of a message written into a cell's output. +/// +/// +/// The prefix is one entry rather than the first word of thirty, so a translator sets it once +/// and every message that carries it reads the same. It is a placeholder rather than something +/// glued on the front, because a language may not put it there at all. +/// +internal static class CellText +{ + /// Marks a message about something a cell could not carry out. + public static string Error(string message) => string.Format(Strings.Prefix_Error, message); + + /// Marks a message about something that worked but is worth knowing. + public static string Warning(string message) => string.Format(Strings.Prefix_Warning, message); +} diff --git a/src/Verso/Localization/VersoCultures.cs b/src/Verso/Localization/VersoCultures.cs new file mode 100644 index 00000000..258103a8 --- /dev/null +++ b/src/Verso/Localization/VersoCultures.cs @@ -0,0 +1,167 @@ +using System.Globalization; + +namespace Verso.Localization; + +/// +/// The languages Verso ships an interface in, and the rules for picking one. +/// +/// +/// Every host answers the same question differently: the CLI has a --language option, the +/// web server has an Accept-Language header, the editor extension has a setting. They all +/// end up here so that the answer is the same wherever it was asked, and so that adding a +/// language is one entry rather than a search through four projects. +/// +public static class VersoCultures +{ + /// The language used when nothing else resolves, and the one every string is written in. + public const string Default = "en"; + + /// + /// Environment variable read when no language was requested explicitly. It exists for + /// containers and CI, where passing an option to every invocation is awkward. + /// + public const string EnvironmentVariable = "VERSO_LANGUAGE"; + + /// + /// A generated stand-in for a real translation. Every string is accented and bracketed, so + /// running the interface in it makes anything still in plain English obvious at a glance, + /// and the padding shows where a longer translation would be clipped. It is deliberately + /// absent from so it never appears in a language picker; ask for it + /// by name. + /// + public const string Pseudo = "qps-Ploc"; + + /// + /// The command-line option every host accepts to name a language. + /// + public const string Option = "--language"; + + /// + /// The languages offered in the interface, in the order a picker should list them. + /// + public static IReadOnlyList Supported { get; } = new[] + { + "en", + "de", + "es", + "ja", + "zh-Hans", + }; + + /// + /// Resolves the language to use from an explicit request, falling back through the + /// environment variable and the operating system to English. + /// + /// + /// A language tag from a command-line option or a setting, or null when none was given. + /// The literal string auto is treated the same as null, because that is what the + /// editor setting stores for "decide for me". + /// + /// A culture that has translations, or the invariant-language English culture. + public static CultureInfo Resolve(string? requested) + { + if (TryMatch(requested, out var explicitly)) + return explicitly; + + if (TryMatch(Environment.GetEnvironmentVariable(EnvironmentVariable), out var fromEnvironment)) + return fromEnvironment; + + if (TryMatch(CultureInfo.CurrentUICulture.Name, out var fromSystem)) + return fromSystem; + + return new CultureInfo(Default); + } + + /// + /// Matches a language tag against the shipped set, narrowing a regional tag to its language + /// where there is no regional translation. + /// + /// + /// de-AT resolves to de, and zh-CN reaches zh-Hans through its + /// parent chain, which is why this walks parents rather than comparing prefixes: the tag for + /// simplified Chinese shares no prefix with the tag a browser is likely to send. + /// + /// A language tag, or null. + /// The matched culture, or null when there is no match. + /// true when the tag maps onto a shipped language. + public static bool TryMatch(string? tag, out CultureInfo culture) + { + culture = null!; + + if (string.IsNullOrWhiteSpace(tag) || tag.Equals("auto", StringComparison.OrdinalIgnoreCase)) + return false; + + CultureInfo candidate; + try + { + candidate = new CultureInfo(tag.Trim()); + } + catch (CultureNotFoundException) + { + return false; + } + + // The pseudo-locale is a real translation as far as the resource system is concerned, + // it is just not one anybody should be offered. + if (string.Equals(candidate.Name, Pseudo, StringComparison.OrdinalIgnoreCase)) + { + culture = candidate; + return true; + } + + for (var walk = candidate; !string.IsNullOrEmpty(walk.Name); walk = walk.Parent) + { + foreach (var supported in Supported) + { + if (string.Equals(walk.Name, supported, StringComparison.OrdinalIgnoreCase)) + { + culture = new CultureInfo(supported); + return true; + } + } + } + + return false; + } + + /// + /// Reads the language named by out of raw process arguments. + /// + /// + /// A scan rather than a parse, because the callers that need this need it before parsing is + /// possible: command help is written while the command tree is built, and the notebook host + /// has no parser at all. Both --language de and --language=de are accepted, and + /// anything else is left alone for a real parser to report in the ordinary way. + /// + /// The process arguments. + /// The requested language tag, or null when the option is absent. + public static string? FromArguments(IReadOnlyList args) + { + for (var i = 0; i < args.Count; i++) + { + if (args[i].StartsWith(Option + "=", StringComparison.Ordinal)) + return args[i][(Option.Length + 1)..]; + + if (args[i].Equals(Option, StringComparison.Ordinal) && i + 1 < args.Count) + return args[i + 1]; + } + + return null; + } + + /// + /// Applies a language to the interface without touching how numbers and dates are formatted. + /// + /// + /// The two are deliberately separate. A notebook's own results are data, and this process + /// runs the kernels that produce them, so moving the formatting culture would change a cell's + /// output because somebody picked a menu language. Hosts that run kernels elsewhere, such as + /// the editor extension, can afford to move both and do. + /// + /// The language to display the interface in. + public static void ApplyUiCulture(CultureInfo culture) + { + CultureInfo.DefaultThreadCurrentUICulture = culture; + CultureInfo.CurrentUICulture = culture; + } +} diff --git a/src/Verso/MagicCommands/AboutMagicCommand.cs b/src/Verso/MagicCommands/AboutMagicCommand.cs index 65837e08..80333f93 100644 --- a/src/Verso/MagicCommands/AboutMagicCommand.cs +++ b/src/Verso/MagicCommands/AboutMagicCommand.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using Verso.Abstractions; +using Verso.Resources; namespace Verso.MagicCommands; @@ -12,14 +13,14 @@ public sealed class AboutMagicCommand : IMagicCommand // --- IExtension (explicit for descriptive Name) --- public string ExtensionId => "verso.magic.about"; - string IExtension.Name => "About Magic Command"; + string IExtension.Name => Strings.Magic_About; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; // --- IMagicCommand --- public string Name => "about"; - public string Description => "Displays Verso version, runtime information, and loaded extensions."; + public string Description => Strings.Magic_About_Description; public IReadOnlyList Parameters => Array.Empty(); public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; @@ -36,15 +37,15 @@ public async Task ExecuteAsync(string arguments, IMagicCommandContext context) var lines = new List { $"Verso v{versoVersion}", - $"Runtime: {framework}", - $"OS: {os}", + string.Format(Strings.Magic_About_Runtime, framework), + string.Format(Strings.Magic_About_Os, os), "" }; var extensions = context.ExtensionHost.GetLoadedExtensions(); if (extensions.Count > 0) { - lines.Add("Loaded extensions:"); + lines.Add(Strings.Magic_About_LoadedExtensions); foreach (var ext in extensions) { lines.Add($" {ext.ExtensionId} ({ext.Name}) v{ext.Version}"); @@ -52,7 +53,7 @@ public async Task ExecuteAsync(string arguments, IMagicCommandContext context) } else { - lines.Add("No extensions loaded."); + lines.Add(Strings.Magic_About_NoExtensions); } var output = new CellOutput("text/plain", string.Join(Environment.NewLine, lines)); diff --git a/src/Verso/MagicCommands/ExtensionMagicCommand.cs b/src/Verso/MagicCommands/ExtensionMagicCommand.cs index 4874a766..e83f91f8 100644 --- a/src/Verso/MagicCommands/ExtensionMagicCommand.cs +++ b/src/Verso/MagicCommands/ExtensionMagicCommand.cs @@ -2,6 +2,8 @@ using Verso.Abstractions; using Verso.Extensions; using Verso.Kernels; +using Verso.Localization; +using Verso.Resources; namespace Verso.MagicCommands; @@ -21,19 +23,19 @@ public sealed class ExtensionMagicCommand : IMagicCommand // --- IExtension (explicit for descriptive Name) --- public string ExtensionId => "verso.magic.extension"; - string IExtension.Name => "Extension Magic Command"; + string IExtension.Name => Strings.Magic_Extension; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; // --- IMagicCommand --- public string Name => "extension"; - public string Description => "Installs a NuGet package or loads a local assembly containing Verso extensions."; + public string Description => Strings.Magic_Extension_Description; public IReadOnlyList Parameters { get; } = new[] { - new ParameterDefinition("packageIdOrPath", "A NuGet package ID or path to a local .dll file.", typeof(string), IsRequired: true), - new ParameterDefinition("version", "Optional package version (NuGet only).", typeof(string)) + new ParameterDefinition("packageIdOrPath", Strings.Magic_Extension_Param_PackageIdOrPath, typeof(string), IsRequired: true), + new ParameterDefinition("version", Strings.Magic_Extension_Param_Version, typeof(string)) }; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; @@ -47,7 +49,7 @@ public async Task ExecuteAsync(string arguments, IMagicCommandContext context) { await context.WriteOutputAsync(new CellOutput( "text/plain", - "Usage: #!extension [Version] or #!extension ", + Strings.Magic_Extension_Usage, IsError: true)) .ConfigureAwait(false); context.SuppressExecution = true; @@ -79,7 +81,7 @@ private async Task ExecuteLocalAsync(string input, IMagicCommandContext context) if (extensionHost?.IsExtensionPackageLoaded(resolvedPath) == true) { await context.WriteOutputAsync(new CellOutput( - "text/plain", $"Extension assembly '{Path.GetFileName(resolvedPath)}' is already loaded.")) + "text/plain", string.Format(Strings.Magic_Extension_AlreadyLoadedAssembly, Path.GetFileName(resolvedPath)))) .ConfigureAwait(false); return; } @@ -88,7 +90,7 @@ await context.WriteOutputAsync(new CellOutput( { await context.WriteOutputAsync(new CellOutput( "text/plain", - $"Error: Extension assembly not found: {resolvedPath}", + CellText.Error(string.Format(Strings.Magic_Extension_AssemblyNotFound, resolvedPath)), IsError: true)) .ConfigureAwait(false); context.SuppressExecution = true; @@ -109,7 +111,7 @@ await context.WriteOutputAsync(new CellOutput( { var consentInfo = new List { - new(Path.GetFileName(resolvedPath), null, "session-generated local assembly") + new(Path.GetFileName(resolvedPath), null, Strings.Magic_Extension_ConsentReason_SessionGenerated) }; var approved = await extensionHost.RequestExtensionConsentAsync( @@ -119,7 +121,7 @@ await context.WriteOutputAsync(new CellOutput( { await context.WriteOutputAsync(new CellOutput( "text/plain", - $"Extension '{Path.GetFileName(resolvedPath)}' was not approved. Skipping.")) + string.Format(Strings.Magic_Extension_NotApproved, Path.GetFileName(resolvedPath)))) .ConfigureAwait(false); return; } @@ -127,7 +129,7 @@ await context.WriteOutputAsync(new CellOutput( } await context.WriteOutputAsync(new CellOutput( - "text/plain", $"Loading extension from '{Path.GetFileName(resolvedPath)}'...")) + "text/plain", string.Format(Strings.Magic_Extension_Loading, Path.GetFileName(resolvedPath)))) .ConfigureAwait(false); try @@ -153,15 +155,17 @@ await context.WriteOutputAsync(new CellOutput( { await context.WriteOutputAsync(new CellOutput( "text/plain", - $"Warning: '{Path.GetFileName(resolvedPath)}' loaded but contains no [VersoExtension] types. " + - "The assembly is still available as a reference.")) + CellText.Warning(string.Format( + Strings.Magic_Extension_NoTypes, Path.GetFileName(resolvedPath))))) .ConfigureAwait(false); } else { await context.WriteOutputAsync(new CellOutput( "text/plain", - $"Loaded '{Path.GetFileName(resolvedPath)}' ({extensionsRegistered} extension{(extensionsRegistered == 1 ? "" : "s")} registered)")) + string.Format( + Plural.Of(extensionsRegistered, Strings.Magic_Extension_Loaded_One, Strings.Magic_Extension_Loaded_Other), + Path.GetFileName(resolvedPath), extensionsRegistered))) .ConfigureAwait(false); } } @@ -169,7 +173,7 @@ await context.WriteOutputAsync(new CellOutput( { await context.WriteOutputAsync(new CellOutput( "text/plain", - $"Error: '{Path.GetFileName(resolvedPath)}' is not a valid .NET assembly.", + CellText.Error(string.Format(Strings.Magic_Extension_NotAnAssembly, Path.GetFileName(resolvedPath))), IsError: true)) .ConfigureAwait(false); context.SuppressExecution = true; @@ -178,7 +182,7 @@ await context.WriteOutputAsync(new CellOutput( { await context.WriteOutputAsync(new CellOutput( "text/plain", - $"Error loading extensions from '{Path.GetFileName(resolvedPath)}': {ex.Message}", + string.Format(Strings.Magic_Extension_LoadFailed, Path.GetFileName(resolvedPath), ex.Message), IsError: true)) .ConfigureAwait(false); context.SuppressExecution = true; @@ -191,7 +195,7 @@ await context.WriteOutputAsync(new CellOutput( { await context.WriteOutputAsync(new CellOutput( "text/plain", - $"Error loading '{Path.GetFileName(resolvedPath)}': {ex.Message}", + string.Format(Strings.Magic_Extension_LoadFailedGeneric, Path.GetFileName(resolvedPath), ex.Message), IsError: true)) .ConfigureAwait(false); context.SuppressExecution = true; @@ -215,7 +219,7 @@ private async Task ExecuteNuGetAsync(string input, IMagicCommandContext context) if (extensionHost?.IsExtensionPackageLoaded(packageId) == true) { await context.WriteOutputAsync(new CellOutput( - "text/plain", $"Extension package '{packageId}' is already loaded.")) + "text/plain", string.Format(Strings.Magic_Extension_AlreadyLoadedPackage, packageId))) .ConfigureAwait(false); return; } @@ -234,7 +238,7 @@ await context.WriteOutputAsync(new CellOutput( if (!approved) { await context.WriteOutputAsync(new CellOutput( - "text/plain", $"Extension '{packageId}' was not approved. Skipping.")) + "text/plain", string.Format(Strings.Magic_Extension_NotApproved, packageId))) .ConfigureAwait(false); return; } @@ -245,8 +249,8 @@ await context.WriteOutputAsync(new CellOutput( await context.WriteOutputAsync(new CellOutput( "text/plain", version is not null - ? $"Resolving extension package '{packageId}' version '{version}'..." - : $"Resolving extension package '{packageId}'...")) + ? string.Format(Strings.Magic_Extension_ResolvingVersion, packageId, version) + : string.Format(Strings.Magic_Extension_Resolving, packageId))) .ConfigureAwait(false); try @@ -297,7 +301,9 @@ version is not null await context.WriteOutputAsync(new CellOutput( "text/plain", - $"Installed '{result.PackageId}' {result.ResolvedVersion} ({extensionsRegistered} extension{(extensionsRegistered == 1 ? "" : "s")} registered)")) + string.Format( + Plural.Of(extensionsRegistered, Strings.Magic_Extension_Installed_One, Strings.Magic_Extension_Installed_Other), + result.PackageId, result.ResolvedVersion, extensionsRegistered))) .ConfigureAwait(false); } catch (OperationCanceledException) @@ -308,7 +314,7 @@ await context.WriteOutputAsync(new CellOutput( { await context.WriteOutputAsync(new CellOutput( "text/plain", - $"Failed to resolve extension package '{packageId}': {ex.Message}", + string.Format(Strings.Magic_Extension_ResolveFailed, packageId, ex.Message), IsError: true)) .ConfigureAwait(false); context.SuppressExecution = true; diff --git a/src/Verso/MagicCommands/ImportMagicCommand.cs b/src/Verso/MagicCommands/ImportMagicCommand.cs index 90cd70d3..95e39a8f 100644 --- a/src/Verso/MagicCommands/ImportMagicCommand.cs +++ b/src/Verso/MagicCommands/ImportMagicCommand.cs @@ -2,6 +2,8 @@ using Verso.Abstractions; using Verso.Extensions; using Verso.Parameters; +using Verso.Localization; +using Verso.Resources; namespace Verso.MagicCommands; @@ -32,20 +34,20 @@ public sealed class ImportMagicCommand : IMagicCommand // --- IExtension --- public string ExtensionId => "verso.magic.import"; - string IExtension.Name => "Import Magic Command"; + string IExtension.Name => Strings.Magic_Import; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; // --- IMagicCommand --- public string Name => "import"; - public string Description => "Imports another notebook file and executes its cells (any cell type other than markdown or raw), with optional parameter overrides. Pass --show-output to display the imported cells' output."; + public string Description => Strings.Magic_Import_Description; public IReadOnlyList Parameters { get; } = new[] { - new ParameterDefinition("path", "Path to the notebook file to import.", typeof(string), IsRequired: true), - new ParameterDefinition("--param", "Parameter override in name=value format. May be repeated.", typeof(string), IsRequired: false), - new ParameterDefinition("--show-output", "Display the output produced by the imported cells. Off by default.", typeof(bool), IsRequired: false) + new ParameterDefinition("path", Strings.Magic_Import_Param_Path, typeof(string), IsRequired: true), + new ParameterDefinition("--param", Strings.Magic_Import_Param_Param, typeof(string), IsRequired: false), + new ParameterDefinition("--show-output", Strings.Magic_Import_Param_ShowOutput, typeof(bool), IsRequired: false) }; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; @@ -58,7 +60,7 @@ public async Task ExecuteAsync(string arguments, IMagicCommandContext context) if (string.IsNullOrWhiteSpace(arguments)) { await context.WriteOutputAsync(new CellOutput("text/plain", - "Error: #!import requires a file path. Usage: #!import [--param name=value ...]", + CellText.Error(Strings.Magic_Import_Usage), IsError: true)).ConfigureAwait(false); return; } @@ -72,7 +74,7 @@ await context.WriteOutputAsync(new CellOutput("text/plain", if (!File.Exists(resolvedPath)) { await context.WriteOutputAsync(new CellOutput("text/plain", - $"Error: File not found: {resolvedPath}", IsError: true)) + CellText.Error(string.Format(Strings.Magic_Import_FileNotFound, resolvedPath)), IsError: true)) .ConfigureAwait(false); return; } @@ -99,8 +101,8 @@ await ImportNotebookAsync(resolvedPath, serializer, paramOverrides, showOutput, var supportedExtensions = GetSupportedExtensions(context.ExtensionHost); await context.WriteOutputAsync(CellOutput.Error( - $"No serializer or kernel found for '{Path.GetFileName(resolvedPath)}'. " + - $"Supported formats: {supportedExtensions}")).ConfigureAwait(false); + string.Format(Strings.Magic_Import_NoSerializer, + Path.GetFileName(resolvedPath), supportedExtensions))).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -109,7 +111,7 @@ await context.WriteOutputAsync(CellOutput.Error( catch (Exception ex) { await context.WriteOutputAsync(new CellOutput("text/plain", - $"Error importing notebook: {ex.Message}", IsError: true)) + string.Format(Strings.Magic_Import_Failed, ex.Message), IsError: true)) .ConfigureAwait(false); } } @@ -134,7 +136,7 @@ private async Task ImportNotebookAsync( { var importedDirectives = directives .Select(e => new ExtensionConsentInfo(e.PackageId, e.Version, - $"imported from {Path.GetFileName(resolvedPath)}")) + string.Format(Strings.Magic_Import_ConsentReason, Path.GetFileName(resolvedPath)))) .ToList(); var approved = await context.ExtensionHost.RequestExtensionConsentAsync( @@ -182,9 +184,15 @@ private async Task ImportNotebookAsync( executedCount++; } - var summary = $"Imported {executedCount} cell{(executedCount == 1 ? "" : "s")} from {Path.GetFileName(resolvedPath)}"; - if (failedCount > 0) - summary += $" ({failedCount} failed)"; + // The count is written out on its own and goes in as one argument, so the sentence + // around it needs one entry rather than a singular and a plural of the whole thing. + var cells = string.Format( + Plural.Of(executedCount, Strings.Magic_Import_CellCount_One, Strings.Magic_Import_CellCount_Other), + executedCount); + var fileName = Path.GetFileName(resolvedPath); + var summary = failedCount > 0 + ? string.Format(Strings.Magic_Import_SummaryWithFailures, cells, fileName, failedCount) + : string.Format(Strings.Magic_Import_Summary, cells, fileName); await context.WriteOutputAsync(new CellOutput("text/plain", summary, IsError: failedCount > 0)) .ConfigureAwait(false); @@ -268,10 +276,12 @@ private async Task ImportSourceFileAsync( var fileName = Path.GetFileName(resolvedPath); var magicCount = magicLines.Count; var summary = magicCount > 0 - ? $"Imported {fileName} ({magicCount} directive{(magicCount == 1 ? "" : "s")} extracted)" - : $"Imported {fileName}"; + ? string.Format(Strings.Magic_Import_SourceSummaryDirectives, fileName, string.Format( + Plural.Of(magicCount, Strings.Magic_Import_DirectiveCount_One, Strings.Magic_Import_DirectiveCount_Other), + magicCount)) + : string.Format(Strings.Magic_Import_SourceSummary, fileName); if (failed) - summary += " (execution failed)"; + summary = string.Format(Strings.Magic_Import_ExecutionFailed, summary); await context.WriteOutputAsync(new CellOutput("text/plain", summary, IsError: failed)) .ConfigureAwait(false); @@ -422,7 +432,8 @@ internal static (string Path, Dictionary Params, bool ShowOutput if (ParameterValueParser.TryParse(def.Type, raw, out var typed, out var error) && typed is not null) variables.Set(name, typed); else - return $"Error: Invalid value for parameter '{name}' ({def.Type}): {error}"; + return CellText.Error(string.Format( + Strings.Magic_Import_InvalidParameter, name, def.Type, error)); } else { @@ -460,8 +471,9 @@ internal static (string Path, Dictionary Params, bool ShowOutput } if (missing.Count > 0) - return $"Error: Missing required parameter{(missing.Count > 1 ? "s" : "")} " + - $"for imported notebook:\n{string.Join("\n", missing)}"; + return CellText.Error(string.Format( + Plural.Of(missing.Count, Strings.Magic_Import_MissingRequired_One, Strings.Magic_Import_MissingRequired_Other), + string.Join("\n", missing))); return null; } diff --git a/src/Verso/MagicCommands/NuGetMagicCommand.cs b/src/Verso/MagicCommands/NuGetMagicCommand.cs index 8cb2d151..1ccdc67d 100644 --- a/src/Verso/MagicCommands/NuGetMagicCommand.cs +++ b/src/Verso/MagicCommands/NuGetMagicCommand.cs @@ -1,5 +1,6 @@ using Verso.Abstractions; using Verso.Kernels; +using Verso.Resources; namespace Verso.MagicCommands; @@ -15,19 +16,19 @@ public sealed class NuGetMagicCommand : IMagicCommand // --- IExtension (explicit for descriptive Name) --- public string ExtensionId => "verso.magic.nuget"; - string IExtension.Name => "NuGet Magic Command"; + string IExtension.Name => Strings.Magic_NuGet; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; // --- IMagicCommand --- public string Name => "nuget"; - public string Description => "Downloads and references a NuGet package for use in subsequent code."; + public string Description => Strings.Magic_NuGet_Description; public IReadOnlyList Parameters { get; } = new[] { - new ParameterDefinition("packageId", "The NuGet package ID.", typeof(string), IsRequired: true), - new ParameterDefinition("version", "Optional package version.", typeof(string)) + new ParameterDefinition("packageId", Strings.Magic_NuGet_Param_PackageId, typeof(string), IsRequired: true), + new ParameterDefinition("version", Strings.Magic_NuGet_Param_Version, typeof(string)) }; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; @@ -40,7 +41,7 @@ public async Task ExecuteAsync(string arguments, IMagicCommandContext context) if (string.IsNullOrWhiteSpace(arguments)) { await context.WriteOutputAsync(new CellOutput( - "text/plain", "Usage: #!nuget [Version]", IsError: true)) + "text/plain", Strings.Magic_NuGet_Usage, IsError: true)) .ConfigureAwait(false); context.SuppressExecution = true; return; @@ -53,8 +54,8 @@ await context.WriteOutputAsync(new CellOutput( await context.WriteOutputAsync(new CellOutput( "text/plain", version is not null - ? $"Resolving NuGet package '{packageId}' version '{version}'..." - : $"Resolving NuGet package '{packageId}'...")) + ? string.Format(Strings.Magic_NuGet_ResolvingVersion, packageId, version) + : string.Format(Strings.Magic_NuGet_Resolving, packageId))) .ConfigureAwait(false); try @@ -82,14 +83,14 @@ version is not null await context.WriteOutputAsync(new CellOutput( "text/plain", - $"Installed '{result.PackageId}', {result.ResolvedVersion}")) + string.Format(Strings.Magic_NuGet_Installed, result.PackageId, result.ResolvedVersion))) .ConfigureAwait(false); } catch (Exception ex) { await context.WriteOutputAsync(new CellOutput( "text/plain", - $"Failed to resolve NuGet package '{packageId}': {ex.Message}", + string.Format(Strings.Magic_NuGet_ResolveFailed, packageId, ex.Message), IsError: true)) .ConfigureAwait(false); context.SuppressExecution = true; diff --git a/src/Verso/MagicCommands/RestartMagicCommand.cs b/src/Verso/MagicCommands/RestartMagicCommand.cs index aa010c8b..496360ff 100644 --- a/src/Verso/MagicCommands/RestartMagicCommand.cs +++ b/src/Verso/MagicCommands/RestartMagicCommand.cs @@ -1,4 +1,5 @@ using Verso.Abstractions; +using Verso.Resources; namespace Verso.MagicCommands; @@ -11,18 +12,18 @@ public sealed class RestartMagicCommand : IMagicCommand // --- IExtension (explicit for descriptive Name) --- public string ExtensionId => "verso.magic.restart"; - string IExtension.Name => "Restart Magic Command"; + string IExtension.Name => Strings.Magic_Restart; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; // --- IMagicCommand --- public string Name => "restart"; - public string Description => "Restarts the specified kernel, or the default kernel if no argument is given."; + public string Description => Strings.Magic_Restart_Description; public IReadOnlyList Parameters { get; } = new[] { - new ParameterDefinition("kernelId", "The language ID of the kernel to restart.", typeof(string)) + new ParameterDefinition("kernelId", Strings.Magic_Restart_Param_KernelId, typeof(string)) }; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; @@ -37,8 +38,8 @@ public async Task ExecuteAsync(string arguments, IMagicCommandContext context) await context.Notebook.RestartKernelAsync(kernelId).ConfigureAwait(false); var message = kernelId is not null - ? $"Kernel '{kernelId}' restarted." - : "Default kernel restarted."; + ? string.Format(Strings.Magic_Restart_Done, kernelId) + : Strings.Magic_Restart_DoneDefault; await context.WriteOutputAsync(new CellOutput("text/plain", message)).ConfigureAwait(false); } diff --git a/src/Verso/MagicCommands/TimeMagicCommand.cs b/src/Verso/MagicCommands/TimeMagicCommand.cs index 0bc12049..78da861f 100644 --- a/src/Verso/MagicCommands/TimeMagicCommand.cs +++ b/src/Verso/MagicCommands/TimeMagicCommand.cs @@ -1,5 +1,6 @@ using Verso.Abstractions; using Verso.Contexts; +using Verso.Resources; namespace Verso.MagicCommands; @@ -12,14 +13,14 @@ public sealed class TimeMagicCommand : IMagicCommand // --- IExtension (explicit for descriptive Name) --- public string ExtensionId => "verso.magic.time"; - string IExtension.Name => "Time Magic Command"; + string IExtension.Name => Strings.Magic_Time; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; // --- IMagicCommand --- public string Name => "time"; - public string Description => "Reports elapsed wall-clock time after cell execution."; + public string Description => Strings.Magic_Time_Description; public IReadOnlyList Parameters => Array.Empty(); public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; diff --git a/src/Verso/Parameters/ParameterValueParser.cs b/src/Verso/Parameters/ParameterValueParser.cs index 7d422120..5ba0bab1 100644 --- a/src/Verso/Parameters/ParameterValueParser.cs +++ b/src/Verso/Parameters/ParameterValueParser.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Verso.Resources; namespace Verso.Parameters; @@ -33,7 +34,7 @@ public static bool TryParse(string typeId, string value, out object? result, out result = longVal; return true; } - error = $"Expected an integer value, got '{value}'."; + error = string.Format(Strings.Parameters_ExpectedInteger, value); return false; case "float": @@ -42,7 +43,7 @@ public static bool TryParse(string typeId, string value, out object? result, out result = doubleVal; return true; } - error = $"Expected a numeric value, got '{value}'."; + error = string.Format(Strings.Parameters_ExpectedNumber, value); return false; case "bool": @@ -55,7 +56,7 @@ public static bool TryParse(string typeId, string value, out object? result, out result = false; return true; default: - error = $"Expected true/false, yes/no, or 1/0, got '{value}'."; + error = string.Format(Strings.Parameters_ExpectedBoolean, value); return false; } @@ -65,7 +66,7 @@ public static bool TryParse(string typeId, string value, out object? result, out result = dateVal; return true; } - error = $"Expected a date in yyyy-MM-dd format, got '{value}'."; + error = string.Format(Strings.Parameters_ExpectedDate, value); return false; case "datetime": @@ -79,11 +80,11 @@ public static bool TryParse(string typeId, string value, out object? result, out result = dtoVal; return true; } - error = $"Expected an ISO 8601 datetime value, got '{value}'."; + error = string.Format(Strings.Parameters_ExpectedDateTime, value); return false; default: - error = $"Unknown parameter type '{typeId}'."; + error = string.Format(Strings.Parameters_UnknownType, typeId); return false; } } diff --git a/src/Verso/Resources/Strings.de.resx b/src/Verso/Resources/Strings.de.resx new file mode 100644 index 00000000..98804b87 --- /dev/null +++ b/src/Verso/Resources/Strings.de.resx @@ -0,0 +1,747 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Zellenausgabe löschen + + + Löscht die Ausgabe der ausgewählten Zelle. + + + Ausgabe löschen + + + Ausgaben löschen + + + Löscht alle Zellenausgaben im Notebook. + + + HTML exportieren + + + Exportiert das Notebook als eigenständiges HTML-Dokument. + + + Markdown exportieren + + + Exportiert das Notebook als Markdown-Dokument. + + + Verso exportieren + + + Exportiert ein auf Markdown basierendes Notebook als native .verso-Datei. + + + Kernel neu starten + + + Beim Neustart des Kernels gehen alle Variablen und der Ausführungszustand verloren. Jetzt neu starten? + + + Startet den aktiven Sprachkernel neu. + + + Alle ausführen + + + Führt alle Zellen im Notebook aus. + + + Zelle ausführen + + + Führt die ausgewählten Zellen aus. + + + Layout wechseln + + + Wechselt der Reihe nach zwischen den verfügbaren Layout-Engines. + + + Design wechseln + + + Wechselt der Reihe nach zwischen den verfügbaren Designs. + + + Code + + + HTML-Zellentyp + + + HTML-Zellentyp zum Schreiben von reinem HTML mit @variable-Ersetzung. + + + Markdown-Zellentyp + + + Markdown-Textzellen, die mit Markdig als HTML dargestellt werden. + + + Mermaid-Zellentyp + + + Mermaid-Zellentyp zum Erstellen von Diagrammen mit der Syntax von mermaid.js. + + + Parameter-Zellentyp + + + Zeigt und verwaltet die Parameterdefinitionen des Notebooks als interaktives Formular. + + + Parameter + + + Zelle + + + lokale Datei: {0} + + + Marketplace + + + vom Notebook benötigte Erweiterungen + + + Der Export als Polyglot-Notebook (.dib) wird nicht unterstützt. Verwenden Sie das native Verso-Format. + + + Erweiterungsassembly nicht gefunden: {0} + + + Erweiterungsverzeichnis nicht gefunden: {0} + + + Interaktive Eingaben werden in dieser Umgebung nicht unterstützt. + + + Das Jupyter-Notebook konnte nicht gelesen werden. + + + Das Layout '{0}' wurde nicht gefunden. + + + Es wurde keine Kernel-ID angegeben und es ist kein Standardkernel konfiguriert. + + + Für die Sprache '{0}' ist kein Kernel registriert. + + + Das Paket '{0}' v{1} konnte nicht heruntergeladen werden. + + + Das Design '{0}' wurde nicht gefunden. + + + Das .verso-Dokument konnte nicht deserialisiert werden. + + + ... ({0} weitere Zeile) + + + ... ({0} weitere Zeilen) + + + Auflistungs-Formatierer + + + Formatiert Auflistungen als HTML-Tabellen. + + + Ausnahmen-Formatierer + + + Formatiert Ausnahmen als strukturiertes HTML. + + + HTML-Formatierer + + + Formatiert Objekte mit einer ToHtml()-Methode als HTML. + + + Bild-Formatierer + + + Formatiert Bytearrays als eingebettete Base64-Bilder. + + + Objekt-Formatierer + + + Formatiert Objekte als HTML-Tabellen mit ihren öffentlichen Eigenschaften und Feldern. + + + Primitivtypen-Formatierer + + + Formatiert primitive und gängige Werttypen als reinen Text. + + + SVG-Formatierer + + + Formatiert SVG-Zeichenfolgen als eingebettetes HTML. + + + C#-Sprachkernel auf Basis von Roslyn-Scripting. + + + HTML-Kernel + + + Führt HTML-Zellen mit @variable-Ersetzung aus. + + + Installierte Pakete + + + Mermaid-Kernel + + + Führt Mermaid-Diagrammzellen mit @variable-Ersetzung aus. + + + Nicht aufgelöste Variable '@{0}'. Im Variablenspeicher wurde keine passende Variable gefunden. + + + {0}-Zelle + + + Dashboard-Layout + + + Rasterbasiertes Dashboard-Layout, das nur die Ausgaben der Zellen zeigt. + + + Dashboard + + + Zum Verschieben ziehen + + + Fügen Sie unten Ihre erste Zelle hinzu. + + + Dieses Notebook ist leer + + + Hier eine {0}-Zelle einfügen + + + Notebook-Layout + + + Lineares Notebook-Layout von oben nach unten mit aktiven, bearbeitbaren Zellen in hervorgehobenen Karten. + + + Notebook + + + Präsentationslayout + + + Präsentationslayout, das nur Ausgaben zeigt, zum Betrachten interaktiver Notebooks. + + + Präsentation + + + Ausführen + + + About-Magic-Command + + + Zeigt die Verso-Version, Angaben zur Laufzeitumgebung und die geladenen Erweiterungen. + + + Geladene Erweiterungen: + + + Keine Erweiterungen geladen. + + + Betriebssystem: {0} + + + Laufzeit: {0} + + + Extension-Magic-Command + + + Die Erweiterungsassembly '{0}' ist bereits geladen. + + + Das Erweiterungspaket '{0}' ist bereits geladen. + + + Erweiterungsassembly nicht gefunden: {0} + + + in der Sitzung erzeugte lokale Assembly + + + Installiert ein NuGet-Paket oder lädt eine lokale Assembly mit Verso-Erweiterungen. + + + '{0}' {1} installiert ({2} Erweiterung registriert) + + + '{0}' {1} installiert ({2} Erweiterungen registriert) + + + Fehler beim Laden der Erweiterungen aus '{0}': {1} + + + Fehler beim Laden von '{0}': {1} + + + '{0}' geladen ({1} Erweiterung registriert) + + + '{0}' geladen ({1} Erweiterungen registriert) + + + Erweiterung wird aus '{0}' geladen... + + + '{0}' wurde geladen, enthält aber keine [VersoExtension]-Typen. Die Assembly steht weiterhin als Verweis zur Verfügung. + + + '{0}' ist keine gültige .NET-Assembly. + + + Die Erweiterung '{0}' wurde nicht bestätigt. Sie wird übersprungen. + + + Eine NuGet-Paket-ID oder ein Pfad zu einer lokalen .dll-Datei. + + + Optionale Paketversion (nur NuGet). + + + Das Erweiterungspaket '{0}' konnte nicht aufgelöst werden: {1} + + + Erweiterungspaket '{0}' wird aufgelöst... + + + Erweiterungspaket '{0}' Version '{1}' wird aufgelöst... + + + Verwendung: #!extension <PackageId> [Version] oder #!extension <path/to/assembly.dll> + + + Import-Magic-Command + + + {0} Zelle + + + {0} Zellen + + + importiert aus {0} + + + Importiert eine andere Notebook-Datei und führt deren Zellen aus (jeden Zellentyp außer markdown und raw), mit optionalen Parameterüberschreibungen. Mit --show-output wird die Ausgabe der importierten Zellen angezeigt. + + + {0} Direktive extrahiert + + + {0} Direktiven extrahiert + + + {0} (Ausführung fehlgeschlagen) + + + Fehler beim Importieren des Notebooks: {0} + + + Datei nicht gefunden: {0} + + + Ungültiger Wert für den Parameter '{0}' ({1}): {2} + + + Fehlender erforderlicher Parameter für das importierte Notebook: +{0} + + + Fehlende erforderliche Parameter für das importierte Notebook: +{0} + + + Für '{0}' wurde kein Serialisierer und kein Kernel gefunden. Unterstützte Formate: {1} + + + Parameterüberschreibung im Format name=value. Mehrfach angebbar. + + + Pfad zur zu importierenden Notebook-Datei. + + + Die von den importierten Zellen erzeugte Ausgabe anzeigen. Standardmäßig aus. + + + {0} importiert + + + {0} importiert ({1}) + + + {0} aus {1} importiert + + + {0} aus {1} importiert ({2} fehlgeschlagen) + + + #!import benötigt einen Dateipfad. Verwendung: #!import <path> [--param name=value ...] + + + NuGet-Magic-Command + + + Lädt ein NuGet-Paket herunter und verweist darauf, damit nachfolgender Code es verwenden kann. + + + '{0}' installiert, {1} + + + Die NuGet-Paket-ID. + + + Optionale Paketversion. + + + Das NuGet-Paket '{0}' konnte nicht aufgelöst werden: {1} + + + NuGet-Paket '{0}' wird aufgelöst... + + + NuGet-Paket '{0}' Version '{1}' wird aufgelöst... + + + Verwendung: #!nuget <PackageId> [Version] + + + Restart-Magic-Command + + + Startet den angegebenen Kernel neu, oder den Standardkernel, wenn kein Argument angegeben wird. + + + Der Kernel '{0}' wurde neu gestartet. + + + Der Standardkernel wurde neu gestartet. + + + Die Sprach-ID des neu zu startenden Kernels. + + + Time-Magic-Command + + + Meldet die verstrichene Zeit nach der Ausführung einer Zelle. + + + Leere Auflistung + + + ({0} Elemente) + + + ({0} Member) + + + ({0} Member) + + + {0} von weiteren Elementen werden angezeigt + + + Parameter hinzufügen + + + Die Parameter wurden übernommen. + + + Abbrechen + + + Standard + + + Beschreibung + + + Name + + + Erforderlich + + + Typ + + + Hinzufügen + + + Standardwert + + + Beschreibung + + + Erwartet wurde true/false, yes/no oder 1/0, erhalten wurde '{0}'. + + + Erwartet wurde ein Datum im Format yyyy-MM-dd, erhalten wurde '{0}'. + + + Erwartet wurde ein Datums-/Zeitwert nach ISO 8601, erhalten wurde '{0}'. + + + Erwartet wurde eine ganze Zahl, erhalten wurde '{0}'. + + + Erwartet wurde ein numerischer Wert, erhalten wurde '{0}'. + + + Ungültiger Wert. + + + Name + + + Keine Parameter definiert. + + + Der Parameter '{0}' wurde nicht gefunden. + + + Parameter entfernen + + + erforderlich + + + Parameter + + + Unbekannter Parametertyp '{0}'. + + + Jupyter-Polyglot-Magic-Splitter + + + Teilt Polyglot-Notebook-Direktiven zum Sprachwechsel in importierten .ipynb-Dateien in eigene Zellen auf. + + + Fehler: {0} + + + Warnung: {0} + + + Eingabe ausblenden + + + Standard: {0} + + + Darstellung + + + Vorschauzeilen der Eingabe + + + Ausgabe + + + Vollständig + + + Ausgeblendet + + + Vorschau + + + Vorschauzeilen der Ausgabe + + + Vorschaustil + + + Zeilen + + + Sichtbarkeit + + + Eigenschaften der Zellendarstellung + + + Stellt Einstellungen zur Darstellung von Ein- und Ausgabe je Zelle bereit. + + + Eigenschaften der Zellensichtbarkeit + + + Stellt im Eigenschaften-Panel layoutbezogene Überschreibungen der Zellensichtbarkeit bereit. + + + Ersatz-Renderer + + + Ersatz + + + HTML-Renderer + + + Stellt HTML-Zellen dar und blendet die Eingabe beim Ausführen aus. + + + Markdown-Renderer + + + Stellt Markdown-Zellen mit Markdig dar. + + + Mermaid-Renderer + + + Stellt Mermaid-Diagrammzellen dar und blendet die Eingabe beim Ausführen aus. + + + Parameter-Renderer + + + Stellt Parameterdefinitionen als interaktives Formular mit typgerechten Eingabefeldern dar. + + + Polyglot-Notebook-Serialisierer + + + Serialisierer für die .dib-Dateien von Polyglot Notebooks, nur zum Importieren. + + + Jupyter-Serialisierer + + + Serialisierer für Jupyter-Notebooks im Format .ipynb (nbformat v4). + + + Markdown-Serialisierer + + + Serialisierer für Markdown-Notebooks (.md), deren Codeblöcke zu Zellen werden. + + + Verso-Serialisierer + + + Serialisierer für Versos eigenes Dateiformat .verso. + + + Verso Dunkel + + + Standardmäßiges dunkles Design für Verso-Notebooks. + + + Verso Hoher Kontrast + + + Barrierefreies Design mit hohem Kontrast und Farbwerten nach WCAG 2.1 AA. + + + Verso Hell + + + Standardmäßiges helles Design für Verso-Notebooks. + + + Eingeklappt + + + Ausgeblendet + + + Nur Ausgabe + + + Sichtbar + + \ No newline at end of file diff --git a/src/Verso/Resources/Strings.es.resx b/src/Verso/Resources/Strings.es.resx new file mode 100644 index 00000000..8046c24c --- /dev/null +++ b/src/Verso/Resources/Strings.es.resx @@ -0,0 +1,747 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Borrar la salida de la celda + + + Borra la salida de la celda seleccionada. + + + Borrar la salida + + + Borrar las salidas + + + Borra todas las salidas de las celdas del cuaderno. + + + Exportar a HTML + + + Exporta el cuaderno como un documento HTML autónomo. + + + Exportar a Markdown + + + Exporta el cuaderno como un documento Markdown. + + + Exportar a Verso + + + Exporta un cuaderno basado en Markdown como un archivo .verso nativo. + + + Reiniciar el kernel + + + Reiniciar el kernel descarta todas las variables y el estado de ejecución. ¿Reiniciar ahora? + + + Reinicia el kernel de lenguaje activo. + + + Ejecutar todo + + + Ejecuta todas las celdas del cuaderno. + + + Ejecutar la celda + + + Ejecuta las celdas seleccionadas. + + + Cambiar el diseño + + + Alterna entre los motores de diseño disponibles. + + + Cambiar el tema + + + Alterna entre los temas disponibles. + + + Código + + + Tipo de celda HTML + + + Tipo de celda HTML para escribir HTML sin procesar con sustitución de @variable. + + + Tipo de celda Markdown + + + Celdas de prosa en Markdown renderizadas a HTML con Markdig. + + + Tipo de celda Mermaid + + + Tipo de celda Mermaid para crear diagramas con la sintaxis de mermaid.js. + + + Tipo de celda de parámetros + + + Muestra y gestiona las definiciones de parámetros del cuaderno como un formulario interactivo. + + + Parámetros + + + celda + + + archivo local: {0} + + + marketplace + + + extensiones que el cuaderno necesita + + + No se admite la exportación a .dib de Polyglot Notebook. Use el formato nativo de Verso. + + + No se encontró el ensamblado de la extensión: {0} + + + No se encontró el directorio de la extensión: {0} + + + Este host no admite la entrada interactiva. + + + No se pudo analizar el cuaderno de Jupyter. + + + No se encontró el diseño '{0}'. + + + No se especificó ningún ID de kernel y no hay ningún kernel predeterminado configurado. + + + No hay ningún kernel registrado para el lenguaje '{0}'. + + + No se pudo descargar el paquete '{0}' v{1}. + + + No se encontró el tema '{0}'. + + + No se pudo deserializar el documento .verso. + + + ... ({0} línea más) + + + ... ({0} líneas más) + + + Formateador de colecciones + + + Da formato a las colecciones como tablas HTML. + + + Formateador de excepciones + + + Da formato a las excepciones como HTML estructurado. + + + Formateador de HTML + + + Da formato como HTML a los objetos que tienen un método ToHtml(). + + + Formateador de imágenes + + + Da formato a las matrices de bytes como imágenes base64 en línea. + + + Formateador de objetos + + + Da formato a los objetos como tablas HTML que muestran las propiedades y los campos públicos. + + + Formateador de tipos primitivos + + + Da formato como texto sin formato a los tipos primitivos y a los tipos de valor comunes. + + + Formateador de SVG + + + Da formato a las cadenas SVG como HTML en línea. + + + Kernel del lenguaje C# basado en el scripting de Roslyn. + + + Kernel de HTML + + + Ejecuta celdas HTML con sustitución de @variable. + + + Paquetes instalados + + + Kernel de Mermaid + + + Ejecuta celdas de diagramas Mermaid con sustitución de @variable. + + + Variable '@{0}' sin resolver. No se encontró ninguna variable coincidente en el almacén de variables. + + + Celda {0} + + + Diseño Dashboard + + + Diseño Dashboard basado en una cuadrícula que muestra solo las salidas de las celdas. + + + Dashboard + + + Arrastrar para mover + + + Añada su primera celda abajo. + + + Este cuaderno está vacío + + + Insertar aquí una celda {0} + + + Diseño de cuaderno + + + Diseño de cuaderno lineal de arriba abajo, con celdas editables en vivo dentro de tarjetas elevadas. + + + Cuaderno + + + Diseño de presentación + + + Diseño de presentación de solo salida para consultar cuadernos interactivos. + + + Presentación + + + Ejecutar + + + Comando mágico About + + + Muestra la versión de Verso, información del runtime y las extensiones cargadas. + + + Extensiones cargadas: + + + No hay extensiones cargadas. + + + SO: {0} + + + Runtime: {0} + + + Comando mágico Extension + + + El ensamblado de extensión '{0}' ya está cargado. + + + El paquete de extensión '{0}' ya está cargado. + + + No se encontró el ensamblado de la extensión: {0} + + + ensamblado local generado en la sesión + + + Instala un paquete de NuGet o carga un ensamblado local que contenga extensiones de Verso. + + + Se instaló '{0}' {1} ({2} extensión registrada) + + + Se instaló '{0}' {1} ({2} extensiones registradas) + + + Error al cargar las extensiones de '{0}': {1} + + + Error al cargar '{0}': {1} + + + Se cargó '{0}' ({1} extensión registrada) + + + Se cargó '{0}' ({1} extensiones registradas) + + + Cargando la extensión desde '{0}'... + + + '{0}' se cargó pero no contiene tipos [VersoExtension]. El ensamblado sigue disponible como referencia. + + + '{0}' no es un ensamblado de .NET válido. + + + La extensión '{0}' no se aprobó. Se omite. + + + Un ID de paquete de NuGet o la ruta a un archivo .dll local. + + + Versión del paquete, opcional (solo para NuGet). + + + No se pudo resolver el paquete de extensión '{0}': {1} + + + Resolviendo el paquete de extensión '{0}'... + + + Resolviendo el paquete de extensión '{0}' versión '{1}'... + + + Uso: #!extension <PackageId> [Version] o #!extension <path/to/assembly.dll> + + + Comando mágico Import + + + {0} celda + + + {0} celdas + + + importado desde {0} + + + Importa otro archivo de cuaderno y ejecuta sus celdas (cualquier tipo de celda que no sea markdown ni raw), con la posibilidad de sobrescribir parámetros. Pase --show-output para mostrar la salida de las celdas importadas. + + + {0} directiva extraída + + + {0} directivas extraídas + + + {0} (la ejecución falló) + + + Error al importar el cuaderno: {0} + + + No se encontró el archivo: {0} + + + Valor no válido para el parámetro '{0}' ({1}): {2} + + + Falta un parámetro obligatorio del cuaderno importado: +{0} + + + Faltan parámetros obligatorios del cuaderno importado: +{0} + + + No se encontró ningún serializador ni kernel para '{0}'. Formatos admitidos: {1} + + + Sobrescritura de parámetro con el formato name=value. Se puede repetir. + + + Ruta al archivo de cuaderno que se va a importar. + + + Mostrar la salida producida por las celdas importadas. Desactivado de forma predeterminada. + + + Importado {0} + + + Importado {0} ({1}) + + + Importado {0} desde {1} + + + Importado {0} desde {1} ({2} con errores) + + + #!import necesita una ruta de archivo. Uso: #!import <path> [--param name=value ...] + + + Comando mágico NuGet + + + Descarga y referencia un paquete de NuGet para usarlo en el código posterior. + + + Se instaló '{0}', {1} + + + El ID del paquete de NuGet. + + + Versión del paquete, opcional. + + + No se pudo resolver el paquete de NuGet '{0}': {1} + + + Resolviendo el paquete de NuGet '{0}'... + + + Resolviendo el paquete de NuGet '{0}' versión '{1}'... + + + Uso: #!nuget <PackageId> [Version] + + + Comando mágico Restart + + + Reinicia el kernel indicado, o el kernel predeterminado si no se pasa ningún argumento. + + + El kernel '{0}' se reinició. + + + El kernel predeterminado se reinició. + + + El ID de lenguaje del kernel que se va a reiniciar. + + + Comando mágico Time + + + Informa del tiempo transcurrido tras ejecutar la celda. + + + Colección vacía + + + ({0} elementos) + + + ({0} miembro) + + + ({0} miembros) + + + Mostrando {0} de más elementos + + + Añadir parámetro + + + Los parámetros se aplicaron correctamente. + + + Cancelar + + + Predeterminado + + + Descripción + + + Nombre + + + Obligatorio + + + Tipo + + + Añadir + + + valor predeterminado + + + descripción + + + Se esperaba true/false, yes/no o 1/0, y se obtuvo '{0}'. + + + Se esperaba una fecha con el formato yyyy-MM-dd, y se obtuvo '{0}'. + + + Se esperaba un valor de fecha y hora ISO 8601, y se obtuvo '{0}'. + + + Se esperaba un valor entero, y se obtuvo '{0}'. + + + Se esperaba un valor numérico, y se obtuvo '{0}'. + + + Valor no válido. + + + nombre + + + No hay parámetros definidos. + + + No se encontró el parámetro '{0}'. + + + Quitar el parámetro + + + obligatorio + + + Parámetros + + + Tipo de parámetro desconocido: '{0}'. + + + Divisor de comandos mágicos de Polyglot para Jupyter + + + Divide en celdas independientes las directivas de cambio de lenguaje de Polyglot Notebook que hay en los archivos .ipynb importados. + + + Error: {0} + + + Advertencia: {0} + + + Contraer la entrada + + + Predeterminado: {0} + + + Visualización + + + Líneas de vista previa de la entrada + + + Salida + + + Completa + + + Oculta + + + Vista previa + + + Líneas de vista previa de la salida + + + Estilo de la vista previa + + + Líneas + + + Visibilidad + + + Propiedades de visualización de la celda + + + Proporciona la configuración de visualización de la entrada y la salida de cada celda. + + + Propiedades de visibilidad de la celda + + + Proporciona las sustituciones de visibilidad de celda por diseño en el panel de propiedades. + + + Renderizador de reserva + + + Reserva + + + Renderizador de HTML + + + Renderiza celdas HTML y contrae la entrada al ejecutarlas. + + + Renderizador de Markdown + + + Renderiza celdas Markdown con Markdig. + + + Renderizador de Mermaid + + + Renderiza celdas de diagramas Mermaid y contrae la entrada al ejecutarlas. + + + Renderizador de parámetros + + + Renderiza las definiciones de parámetros como un formulario interactivo con campos según el tipo. + + + Serializador de Polyglot Notebook + + + Serializador de solo importación para los archivos .dib de Polyglot Notebooks. + + + Serializador de Jupyter + + + Serializador para cuadernos .ipynb de Jupyter (nbformat v4). + + + Serializador de Markdown + + + Serializador para cuadernos Markdown (.md) cuyos bloques de código delimitados son celdas. + + + Serializador de Verso + + + Serializador del formato de archivo propio .verso. + + + Verso Oscuro + + + Tema oscuro predeterminado para los cuadernos de Verso. + + + Verso Alto Contraste + + + Tema de accesibilidad de alto contraste con tokens de color conformes a WCAG 2.1 AA. + + + Verso Claro + + + Tema claro predeterminado para los cuadernos de Verso. + + + Contraída + + + Oculta + + + Solo la salida + + + Visible + + \ No newline at end of file diff --git a/src/Verso/Resources/Strings.ja.resx b/src/Verso/Resources/Strings.ja.resx new file mode 100644 index 00000000..1b194f35 --- /dev/null +++ b/src/Verso/Resources/Strings.ja.resx @@ -0,0 +1,747 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + セルの出力を消去 + + + 選択したセルの出力を消去します。 + + + 出力を消去 + + + すべての出力を消去 + + + ノートブック内のすべてのセルの出力を消去します。 + + + HTML にエクスポート + + + ノートブックを単体で完結する HTML ドキュメントとしてエクスポートします。 + + + Markdown にエクスポート + + + ノートブックを Markdown ドキュメントとしてエクスポートします。 + + + Verso にエクスポート + + + Markdown を元にしたノートブックを、ネイティブの .verso ファイルとしてエクスポートします。 + + + カーネルを再起動 + + + カーネルを再起動すると、すべての変数と実行状態が失われます。再起動しますか? + + + アクティブな言語カーネルを再起動します。 + + + すべて実行 + + + ノートブック内のすべてのセルを実行します。 + + + セルを実行 + + + 選択したセルを実行します。 + + + レイアウトを切り替え + + + 使用できるレイアウトエンジンを順に切り替えます。 + + + テーマを切り替え + + + 使用できるテーマを順に切り替えます。 + + + コード + + + HTML セルタイプ + + + @variable の置き換えに対応した、生の HTML を書くためのセルタイプ。 + + + Markdown セルタイプ + + + Markdig で HTML に変換される Markdown の文章セル。 + + + Mermaid セルタイプ + + + mermaid.js の記法で図を作成するためのセルタイプ。 + + + パラメーターセルタイプ + + + ノートブックのパラメーター定義を対話的なフォームとして表示、管理します。 + + + パラメーター + + + セル + + + ローカルファイル: {0} + + + マーケットプレース + + + ノートブックが必要とする拡張機能 + + + Polyglot Notebook の .dib へのエクスポートはサポートされていません。Verso のネイティブ形式を使ってください。 + + + 拡張機能のアセンブリが見つかりません: {0} + + + 拡張機能のディレクトリが見つかりません: {0} + + + このホストは対話的な入力をサポートしていません。 + + + Jupyter ノートブックを解析できませんでした。 + + + レイアウト '{0}' が見つかりません。 + + + カーネル ID が指定されておらず、既定のカーネルも設定されていません。 + + + 言語 '{0}' に対応するカーネルが登録されていません。 + + + パッケージ '{0}' v{1} をダウンロードできませんでした。 + + + テーマ '{0}' が見つかりません。 + + + .verso ドキュメントを読み込めませんでした。 + + + ... (他 {0} 行) + + + ... (他 {0} 行) + + + コレクションフォーマッター + + + コレクションを HTML の表として整形します。 + + + 例外フォーマッター + + + 例外を構造化された HTML として整形します。 + + + HTML フォーマッター + + + ToHtml() メソッドを持つオブジェクトを HTML として整形します。 + + + 画像フォーマッター + + + バイト配列をインラインの base64 画像として整形します。 + + + オブジェクトフォーマッター + + + オブジェクトのパブリックなプロパティとフィールドを HTML の表として整形します。 + + + プリミティブ型フォーマッター + + + プリミティブ型や一般的な値型をプレーンテキストとして整形します。 + + + SVG フォーマッター + + + SVG の文字列をインライン HTML として整形します。 + + + Roslyn のスクリプティングを利用した C# 言語カーネル。 + + + HTML カーネル + + + @variable の置き換えを行いながら HTML セルを実行します。 + + + インストールされたパッケージ + + + Mermaid カーネル + + + @variable の置き換えを行いながら Mermaid の図のセルを実行します。 + + + 変数 '@{0}' を解決できません。変数ストアに一致する変数がありません。 + + + {0} セル + + + ダッシュボードレイアウト + + + 出力だけを並べる、グリッド形式のダッシュボードレイアウト。 + + + ダッシュボード + + + ドラッグで移動 + + + 下から最初のセルを追加してください。 + + + このノートブックは空です + + + ここに {0} セルを挿入 + + + ノートブックレイアウト + + + 編集できるセルをカードに並べる、上から下へ読む標準のノートブックレイアウト。 + + + ノートブック + + + プレゼンテーションレイアウト + + + 対話的なノートブックを読むための、出力だけを表示するレイアウト。 + + + プレゼンテーション + + + 実行 + + + About マジックコマンド + + + Verso のバージョン、ランタイムの情報、読み込み済みの拡張機能を表示します。 + + + 読み込み済みの拡張機能: + + + 読み込まれている拡張機能はありません。 + + + OS: {0} + + + ランタイム: {0} + + + Extension マジックコマンド + + + 拡張機能のアセンブリ '{0}' は既に読み込まれています。 + + + 拡張機能パッケージ '{0}' は既に読み込まれています。 + + + 拡張機能のアセンブリが見つかりません: {0} + + + このセッションで生成されたローカルアセンブリ + + + NuGet パッケージをインストールするか、Verso の拡張機能を含むローカルのアセンブリを読み込みます。 + + + '{0}' {1} をインストールしました (拡張機能 {2} 件を登録) + + + '{0}' {1} をインストールしました (拡張機能 {2} 件を登録) + + + '{0}' からの拡張機能の読み込みでエラーが発生しました: {1} + + + '{0}' の読み込みでエラーが発生しました: {1} + + + '{0}' を読み込みました (拡張機能 {1} 件を登録) + + + '{0}' を読み込みました (拡張機能 {1} 件を登録) + + + '{0}' から拡張機能を読み込んでいます... + + + '{0}' を読み込みましたが、[VersoExtension] の型が含まれていません。このアセンブリは参照としては引き続き利用できます。 + + + '{0}' は有効な .NET アセンブリではありません。 + + + 拡張機能 '{0}' は許可されませんでした。スキップします。 + + + NuGet パッケージ ID、またはローカルの .dll ファイルのパス。 + + + パッケージのバージョン (NuGet のみ、省略可)。 + + + 拡張機能パッケージ '{0}' を解決できませんでした: {1} + + + 拡張機能パッケージ '{0}' を解決しています... + + + 拡張機能パッケージ '{0}' のバージョン '{1}' を解決しています... + + + 使い方: #!extension <PackageId> [Version] または #!extension <path/to/assembly.dll> + + + Import マジックコマンド + + + {0} 個のセル + + + {0} 個のセル + + + {0} からインポート + + + 別のノートブックファイルを読み込み、そのセル (Markdown と raw 以外のすべてのセルタイプ) を実行します。パラメーターの上書きも指定できます。インポートしたセルの出力を表示するには --show-output を指定してください。 + + + {0} 件のディレクティブを抽出 + + + {0} 件のディレクティブを抽出 + + + {0} (実行に失敗) + + + ノートブックのインポートでエラーが発生しました: {0} + + + ファイルが見つかりません: {0} + + + パラメーター '{0}' ({1}) の値が正しくありません: {2} + + + インポートするノートブックの必須パラメーターが指定されていません: +{0} + + + インポートするノートブックの必須パラメーターが指定されていません: +{0} + + + '{0}' に対応するシリアライザーまたはカーネルが見つかりません。使用できる形式: {1} + + + name=value の形式でパラメーターを上書きします。繰り返し指定できます。 + + + インポートするノートブックファイルのパス。 + + + インポートしたセルが生成した出力を表示します。既定では無効です。 + + + {0} をインポートしました + + + {0} をインポートしました ({1}) + + + {1} から {0} をインポートしました + + + {1} から {0} をインポートしました ({2} 件が失敗) + + + #!import にはファイルパスが必要です。使い方: #!import <path> [--param name=value ...] + + + NuGet マジックコマンド + + + NuGet パッケージをダウンロードし、以降のコードから参照できるようにします。 + + + '{0}' {1} をインストールしました + + + NuGet パッケージの ID。 + + + パッケージのバージョン (省略可)。 + + + NuGet パッケージ '{0}' を解決できませんでした: {1} + + + NuGet パッケージ '{0}' を解決しています... + + + NuGet パッケージ '{0}' のバージョン '{1}' を解決しています... + + + 使い方: #!nuget <PackageId> [Version] + + + Restart マジックコマンド + + + 指定したカーネルを再起動します。引数がない場合は既定のカーネルを再起動します。 + + + カーネル '{0}' を再起動しました。 + + + 既定のカーネルを再起動しました。 + + + 再起動するカーネルの言語 ID。 + + + Time マジックコマンド + + + セルの実行にかかった実時間を報告します。 + + + 空のコレクション + + + ({0} 個の項目) + + + ({0} 個のメンバー) + + + ({0} 個のメンバー) + + + {0} 個を表示中 (さらにあります) + + + パラメーターを追加 + + + パラメーターを適用しました。 + + + キャンセル + + + 既定値 + + + 説明 + + + 名前 + + + 必須 + + + + + + 追加 + + + 既定値 + + + 説明 + + + true/false、yes/no、1/0 のいずれかを指定してください。入力された値: '{0}' + + + yyyy-MM-dd 形式の日付を指定してください。入力された値: '{0}' + + + ISO 8601 形式の日時を指定してください。入力された値: '{0}' + + + 整数を指定してください。入力された値: '{0}' + + + 数値を指定してください。入力された値: '{0}' + + + 値が正しくありません。 + + + 名前 + + + パラメーターは定義されていません。 + + + パラメーター '{0}' が見つかりません。 + + + パラメーターを削除 + + + 必須 + + + パラメーター + + + 不明なパラメーターの型 '{0}' です。 + + + Jupyter Polyglot マジックコマンド分割ツール + + + インポートした .ipynb ファイル内の Polyglot Notebook の言語切り替えディレクティブを、別々のセルに分割します。 + + + エラー: {0} + + + 警告: {0} + + + 入力を折りたたむ + + + 既定: {0} + + + 表示 + + + 入力プレビューの行数 + + + 出力 + + + 全体 + + + 非表示 + + + プレビュー + + + 出力プレビューの行数 + + + プレビューの形式 + + + + + + 表示対象 + + + セルの表示プロパティ + + + セルごとの入力と出力の表示設定を提供します。 + + + セルの表示対象プロパティ + + + プロパティパネルで、レイアウトごとのセルの表示設定を上書きできるようにします。 + + + フォールバックレンダラー + + + フォールバック + + + HTML レンダラー + + + HTML セルを表示し、実行時に入力を折りたたみます。 + + + Markdown レンダラー + + + Markdig を使って Markdown セルを表示します。 + + + Mermaid レンダラー + + + Mermaid の図のセルを表示し、実行時に入力を折りたたみます。 + + + パラメーターレンダラー + + + パラメーターの定義を、型に応じた入力欄を持つ対話的なフォームとして表示します。 + + + Polyglot Notebook シリアライザー + + + Polyglot Notebooks の .dib ファイルを読み込むだけのシリアライザーです。 + + + Jupyter シリアライザー + + + Jupyter の .ipynb ノートブック (nbformat v4) のシリアライザーです。 + + + Markdown シリアライザー + + + コードフェンスをセルとして扱う Markdown (.md) ノートブックのシリアライザーです。 + + + Verso シリアライザー + + + Verso 独自の .verso ファイル形式のシリアライザーです。 + + + Verso ダーク + + + Verso ノートブックの既定のダークテーマ。 + + + Verso ハイコントラスト + + + WCAG 2.1 AA に準拠した配色トークンを使う、アクセシビリティ向けのハイコントラストテーマ。 + + + Verso ライト + + + Verso ノートブックの既定のライトテーマ。 + + + 折りたたみ + + + 非表示 + + + 出力のみ + + + 表示 + + \ No newline at end of file diff --git a/src/Verso/Resources/Strings.qps-Ploc.resx b/src/Verso/Resources/Strings.qps-Ploc.resx new file mode 100644 index 00000000..a32c30c8 --- /dev/null +++ b/src/Verso/Resources/Strings.qps-Ploc.resx @@ -0,0 +1,747 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + [!!Çléàr Çéll Òùtpùt···!!] + + + [!!Çléàrš thé òùtpùt òf thé šéléçtéd çéll.···!!] + + + [!!Çléàr Òùtpùt···!!] + + + [!!Çléàr Òùtpùtš···!!] + + + [!!Çléàrš àll çéll òùtpùtš ïñ thé ñòtébòòk.···!!] + + + [!!Éxpòrt HTML···!!] + + + [!!Éxpòrtš thé ñòtébòòk àš à šélf-çòñtàïñéd HTML dòçùméñt.···!!] + + + [!!Éxpòrt Màrkdòwñ···!!] + + + [!!Éxpòrtš thé ñòtébòòk àš à Màrkdòwñ dòçùméñt.···!!] + + + [!!Éxpòrt Véršò···!!] + + + [!!Éxpòrtš à Màrkdòwñ-bàçkéd ñòtébòòk àš à ñàtïvé .véršò fïlé.···!!] + + + [!!Réštàrt Kérñél···!!] + + + [!!Réštàrtïñg thé kérñél dïšçàrdš àll vàrïàbléš àñd éxéçùtïòñ štàté. Réštàrt ñòw?···!!] + + + [!!Réštàrtš thé àçtïvé làñgùàgé kérñél.···!!] + + + [!!Rùñ Àll···!!] + + + [!!Éxéçùtéš àll çéllš ïñ thé ñòtébòòk.···!!] + + + [!!Rùñ Çéll···!!] + + + [!!Éxéçùtéš thé šéléçtéd çéllš.···!!] + + + [!!Šwïtçh Làÿòùt···!!] + + + [!!Çÿçléš bétwééñ àvàïlàblé làÿòùt éñgïñéš.···!!] + + + [!!Šwïtçh Thémé···!!] + + + [!!Çÿçléš bétwééñ àvàïlàblé théméš.···!!] + + + [!!Çòdé···!!] + + + [!!HTML Çéll Tÿpé···!!] + + + [!!HTML çéll tÿpé fòr àùthòrïñg ràw HTML wïth @vàrïàblé šùbštïtùtïòñ.···!!] + + + [!!Màrkdòwñ Çéll Tÿpé···!!] + + + [!!Màrkdòwñ pròšé çéllš réñdéréd tò HTML wïth Màrkdïg.···!!] + + + [!!Mérmàïd Çéll Tÿpé···!!] + + + [!!Mérmàïd çéll tÿpé fòr çréàtïñg dïàgràmš wïth mérmàïd.jš šÿñtàx.···!!] + + + [!!Pàràmétérš Çéll Tÿpé···!!] + + + [!!Dïšplàÿš àñd màñàgéš ñòtébòòk pàràmétér défïñïtïòñš àš àñ ïñtéràçtïvé fòrm.···!!] + + + [!!Pàràmétérš···!!] + + + [!!çéll···!!] + + + [!!lòçàl fïlé: {0}···!!] + + + [!!màrkétplàçé···!!] + + + [!!ñòtébòòk réqùïréd éxtéñšïòñš···!!] + + + [!!Pòlÿglòt Ñòtébòòk .dïb éxpòrt ïš ñòt šùppòrtéd. Ùšé thé Véršò ñàtïvé fòrmàt.···!!] + + + [!!Éxtéñšïòñ àššémblÿ ñòt fòùñd: {0}···!!] + + + [!!Éxtéñšïòñ dïréçtòrÿ ñòt fòùñd: {0}···!!] + + + [!!Ïñtéràçtïvé ïñpùt ïš ñòt šùppòrtéd bÿ thïš hòšt.···!!] + + + [!!Fàïléd tò pàršé Jùpÿtér ñòtébòòk.···!!] + + + [!!Làÿòùt '{0}' ñòt fòùñd.···!!] + + + [!!Ñò kérñél ÏD špéçïfïéd àñd ñò défàùlt kérñél ïš çòñfïgùréd.···!!] + + + [!!Ñò kérñél régïštéréd fòr làñgùàgé '{0}'.···!!] + + + [!!Fàïléd tò dòwñlòàd pàçkàgé '{0}' v{1}.···!!] + + + [!!Thémé '{0}' ñòt fòùñd.···!!] + + + [!!Fàïléd tò déšérïàlïzé .véršò dòçùméñt.···!!] + + + [!!... ({0} mòré lïñé)···!!] + + + [!!... ({0} mòré lïñéš)···!!] + + + [!!Çòlléçtïòñ Fòrmàttér···!!] + + + [!!Fòrmàtš çòlléçtïòñš àš HTML tàbléš.···!!] + + + [!!Éxçéptïòñ Fòrmàttér···!!] + + + [!!Fòrmàtš éxçéptïòñš àš štrùçtùréd HTML.···!!] + + + [!!HTML Fòrmàttér···!!] + + + [!!Fòrmàtš òbjéçtš wïth à TòHtml() méthòd àš HTML.···!!] + + + [!!Ïmàgé Fòrmàttér···!!] + + + [!!Fòrmàtš bÿté àrràÿš àš ïñlïñé bàšé64 ïmàgéš.···!!] + + + [!!Òbjéçt Fòrmàttér···!!] + + + [!!Fòrmàtš òbjéçtš àš HTML tàbléš šhòwïñg pùblïç pròpértïéš àñd fïéldš.···!!] + + + [!!Prïmïtïvé Fòrmàttér···!!] + + + [!!Fòrmàtš prïmïtïvé àñd çòmmòñ vàlùé tÿpéš àš plàïñ téxt.···!!] + + + [!!ŠVG Fòrmàttér···!!] + + + [!!Fòrmàtš ŠVG štrïñgš àš ïñlïñé HTML.···!!] + + + [!!Ç# làñgùàgé kérñél pòwéréd bÿ Ròšlÿñ šçrïptïñg.···!!] + + + [!!HTML Kérñél···!!] + + + [!!Éxéçùtéš HTML çéllš wïth @vàrïàblé šùbštïtùtïòñ.···!!] + + + [!!Ïñštàlléd Pàçkàgéš···!!] + + + [!!Mérmàïd Kérñél···!!] + + + [!!Éxéçùtéš Mérmàïd dïàgràm çéllš wïth @vàrïàblé šùbštïtùtïòñ.···!!] + + + [!!Ùñréšòlvéd vàrïàblé '@{0}'. Ñò màtçhïñg vàrïàblé fòùñd ïñ thé vàrïàblé štòré.···!!] + + + [!!{0} Çéll···!!] + + + [!!Dàšhbòàrd Làÿòùt···!!] + + + [!!Grïd-bàšéd dàšhbòàrd làÿòùt šhòwïñg òùtpùt-òñlÿ çéllš.···!!] + + + [!!Dàšhbòàrd···!!] + + + [!!Dràg tò mòvé···!!] + + + [!!Àdd ÿòùr fïršt çéll bélòw.···!!] + + + [!!Thïš ñòtébòòk ïš émptÿ···!!] + + + [!!Ïñšért à {0} çéll héré···!!] + + + [!!Ñòtébòòk Làÿòùt···!!] + + + [!!Lïñéàr tòp-tò-bòttòm ñòtébòòk làÿòùt wïth lïvé, édïtàblé çéllš ïñ élévàtéd çàrdš.···!!] + + + [!!Ñòtébòòk···!!] + + + [!!Préšéñtàtïòñ Làÿòùt···!!] + + + [!!Òùtpùt-òñlÿ préšéñtàtïòñ làÿòùt fòr çòñšùmïñg ïñtéràçtïvé ñòtébòòkš.···!!] + + + [!!Préšéñtàtïòñ···!!] + + + [!!Rùñ···!!] + + + [!!Àbòùt Màgïç Çòmmàñd···!!] + + + [!!Dïšplàÿš Véršò véršïòñ, rùñtïmé ïñfòrmàtïòñ, àñd lòàdéd éxtéñšïòñš.···!!] + + + [!!Lòàdéd éxtéñšïòñš:···!!] + + + [!!Ñò éxtéñšïòñš lòàdéd.···!!] + + + [!!ÒŠ: {0}···!!] + + + [!!Rùñtïmé: {0}···!!] + + + [!!Éxtéñšïòñ Màgïç Çòmmàñd···!!] + + + [!!Éxtéñšïòñ àššémblÿ '{0}' ïš àlréàdÿ lòàdéd.···!!] + + + [!!Éxtéñšïòñ pàçkàgé '{0}' ïš àlréàdÿ lòàdéd.···!!] + + + [!!Éxtéñšïòñ àššémblÿ ñòt fòùñd: {0}···!!] + + + [!!šéššïòñ-géñéràtéd lòçàl àššémblÿ···!!] + + + [!!Ïñštàllš à ÑùGét pàçkàgé òr lòàdš à lòçàl àššémblÿ çòñtàïñïñg Véršò éxtéñšïòñš.···!!] + + + [!!Ïñštàlléd '{0}' {1} ({2} éxtéñšïòñ régïštéréd)···!!] + + + [!!Ïñštàlléd '{0}' {1} ({2} éxtéñšïòñš régïštéréd)···!!] + + + [!!Érròr lòàdïñg éxtéñšïòñš fròm '{0}': {1}···!!] + + + [!!Érròr lòàdïñg '{0}': {1}···!!] + + + [!!Lòàdéd '{0}' ({1} éxtéñšïòñ régïštéréd)···!!] + + + [!!Lòàdéd '{0}' ({1} éxtéñšïòñš régïštéréd)···!!] + + + [!!Lòàdïñg éxtéñšïòñ fròm '{0}'...···!!] + + + [!!'{0}' lòàdéd bùt çòñtàïñš ñò [VéršòÉxtéñšïòñ] tÿpéš. Thé àššémblÿ ïš štïll àvàïlàblé àš à référéñçé.···!!] + + + [!!'{0}' ïš ñòt à vàlïd .ÑÉT àššémblÿ.···!!] + + + [!!Éxtéñšïòñ '{0}' wàš ñòt àppròvéd. Škïppïñg.···!!] + + + [!!À ÑùGét pàçkàgé ÏD òr pàth tò à lòçàl .dll fïlé.···!!] + + + [!!Òptïòñàl pàçkàgé véršïòñ (ÑùGét òñlÿ).···!!] + + + [!!Fàïléd tò réšòlvé éxtéñšïòñ pàçkàgé '{0}': {1}···!!] + + + [!!Réšòlvïñg éxtéñšïòñ pàçkàgé '{0}'...···!!] + + + [!!Réšòlvïñg éxtéñšïòñ pàçkàgé '{0}' véršïòñ '{1}'...···!!] + + + [!!Ùšàgé: #!éxtéñšïòñ <PàçkàgéÏd> [Véršïòñ] òr #!éxtéñšïòñ <pàth/tò/àššémblÿ.dll>···!!] + + + [!!Ïmpòrt Màgïç Çòmmàñd···!!] + + + [!!{0} çéll···!!] + + + [!!{0} çéllš···!!] + + + [!!ïmpòrtéd fròm {0}···!!] + + + [!!Ïmpòrtš àñòthér ñòtébòòk fïlé àñd éxéçùtéš ïtš çéllš (àñÿ çéll tÿpé òthér thàñ màrkdòwñ òr ràw), wïth òptïòñàl pàràmétér òvérrïdéš. Pàšš --šhòw-òùtpùt tò dïšplàÿ thé ïmpòrtéd çéllš' òùtpùt.···!!] + + + [!!{0} dïréçtïvé éxtràçtéd···!!] + + + [!!{0} dïréçtïvéš éxtràçtéd···!!] + + + [!!{0} (éxéçùtïòñ fàïléd)···!!] + + + [!!Érròr ïmpòrtïñg ñòtébòòk: {0}···!!] + + + [!!Fïlé ñòt fòùñd: {0}···!!] + + + [!!Ïñvàlïd vàlùé fòr pàràmétér '{0}' ({1}): {2}···!!] + + + [!!Mïššïñg réqùïréd pàràmétér fòr ïmpòrtéd ñòtébòòk: +{0}···!!] + + + [!!Mïššïñg réqùïréd pàràmétérš fòr ïmpòrtéd ñòtébòòk: +{0}···!!] + + + [!!Ñò šérïàlïzér òr kérñél fòùñd fòr '{0}'. Šùppòrtéd fòrmàtš: {1}···!!] + + + [!!Pàràmétér òvérrïdé ïñ ñàmé=vàlùé fòrmàt. Màÿ bé répéàtéd.···!!] + + + [!!Pàth tò thé ñòtébòòk fïlé tò ïmpòrt.···!!] + + + [!!Dïšplàÿ thé òùtpùt pròdùçéd bÿ thé ïmpòrtéd çéllš. Òff bÿ défàùlt.···!!] + + + [!!Ïmpòrtéd {0}···!!] + + + [!!Ïmpòrtéd {0} ({1})···!!] + + + [!!Ïmpòrtéd {0} fròm {1}···!!] + + + [!!Ïmpòrtéd {0} fròm {1} ({2} fàïléd)···!!] + + + [!!#!ïmpòrt réqùïréš à fïlé pàth. Ùšàgé: #!ïmpòrt <pàth> [--pàràm ñàmé=vàlùé ...]···!!] + + + [!!ÑùGét Màgïç Çòmmàñd···!!] + + + [!!Dòwñlòàdš àñd référéñçéš à ÑùGét pàçkàgé fòr ùšé ïñ šùbšéqùéñt çòdé.···!!] + + + [!!Ïñštàlléd '{0}', {1}···!!] + + + [!!Thé ÑùGét pàçkàgé ÏD.···!!] + + + [!!Òptïòñàl pàçkàgé véršïòñ.···!!] + + + [!!Fàïléd tò réšòlvé ÑùGét pàçkàgé '{0}': {1}···!!] + + + [!!Réšòlvïñg ÑùGét pàçkàgé '{0}'...···!!] + + + [!!Réšòlvïñg ÑùGét pàçkàgé '{0}' véršïòñ '{1}'...···!!] + + + [!!Ùšàgé: #!ñùgét <PàçkàgéÏd> [Véršïòñ]···!!] + + + [!!Réštàrt Màgïç Çòmmàñd···!!] + + + [!!Réštàrtš thé špéçïfïéd kérñél, òr thé défàùlt kérñél ïf ñò àrgùméñt ïš gïvéñ.···!!] + + + [!!Kérñél '{0}' réštàrtéd.···!!] + + + [!!Défàùlt kérñél réštàrtéd.···!!] + + + [!!Thé làñgùàgé ÏD òf thé kérñél tò réštàrt.···!!] + + + [!!Tïmé Màgïç Çòmmàñd···!!] + + + [!!Répòrtš élàpšéd wàll-çlòçk tïmé àftér çéll éxéçùtïòñ.···!!] + + + [!!Émptÿ çòlléçtïòñ···!!] + + + [!!({0} ïtémš)···!!] + + + [!!({0} mémbér)···!!] + + + [!!({0} mémbérš)···!!] + + + [!!Šhòwïñg {0} òf mòré ïtémš···!!] + + + [!!Àdd Pàràmétér···!!] + + + [!!Pàràmétérš àpplïéd šùççéššfùllÿ.···!!] + + + [!!Çàñçél···!!] + + + [!!Défàùlt···!!] + + + [!!Déšçrïptïòñ···!!] + + + [!!Ñàmé···!!] + + + [!!Réqùïréd···!!] + + + [!!Tÿpé···!!] + + + [!!Àdd···!!] + + + [!!défàùlt vàlùé···!!] + + + [!!déšçrïptïòñ···!!] + + + [!!Éxpéçtéd trùé/fàlšé, ÿéš/ñò, òr 1/0, gòt '{0}'.···!!] + + + [!!Éxpéçtéd à dàté ïñ ÿÿÿÿ-MM-dd fòrmàt, gòt '{0}'.···!!] + + + [!!Éxpéçtéd àñ ÏŠÒ 8601 dàtétïmé vàlùé, gòt '{0}'.···!!] + + + [!!Éxpéçtéd àñ ïñtégér vàlùé, gòt '{0}'.···!!] + + + [!!Éxpéçtéd à ñùmérïç vàlùé, gòt '{0}'.···!!] + + + [!!Ïñvàlïd vàlùé.···!!] + + + [!!ñàmé···!!] + + + [!!Ñò pàràmétérš défïñéd.···!!] + + + [!!Pàràmétér '{0}' ñòt fòùñd.···!!] + + + [!!Rémòvé pàràmétér···!!] + + + [!!réqùïréd···!!] + + + [!!Pàràmétérš···!!] + + + [!!Ùñkñòwñ pàràmétér tÿpé '{0}'.···!!] + + + [!!Jùpÿtér Pòlÿglòt Màgïç Šplïttér···!!] + + + [!!Šplïtš Pòlÿglòt Ñòtébòòk làñgùàgé-šwïtçhïñg dïréçtïvéš ïñ ïmpòrtéd .ïpÿñb fïléš ïñtò šépàràté çéllš.···!!] + + + [!!Érròr: {0}···!!] + + + [!!Wàrñïñg: {0}···!!] + + + [!!Çòllàpšé ïñpùt···!!] + + + [!!Défàùlt: {0}···!!] + + + [!!Dïšplàÿ···!!] + + + [!!Ïñpùt prévïéw lïñéš···!!] + + + [!!Òùtpùt···!!] + + + [!!Fùll···!!] + + + [!!Hïddéñ···!!] + + + [!!Prévïéw···!!] + + + [!!Òùtpùt prévïéw lïñéš···!!] + + + [!!Prévïéw štÿlé···!!] + + + [!!Lïñéš···!!] + + + [!!Vïšïbïlïtÿ···!!] + + + [!!Çéll Dïšplàÿ Pròpértïéš···!!] + + + [!!Pròvïdéš pér-çéll ïñpùt àñd òùtpùt dïšplàÿ šéttïñgš.···!!] + + + [!!Çéll Vïšïbïlïtÿ Pròpértïéš···!!] + + + [!!Pròvïdéš pér-làÿòùt çéll vïšïbïlïtÿ òvérrïdéš ïñ thé pròpértïéš pàñél.···!!] + + + [!!Fàllbàçk Réñdérér···!!] + + + [!!Fàllbàçk···!!] + + + [!!HTML Réñdérér···!!] + + + [!!Réñdérš HTML çéllš wïth ïñpùt çòllàpšé òñ éxéçùté.···!!] + + + [!!Màrkdòwñ Réñdérér···!!] + + + [!!Réñdérš Màrkdòwñ çéllš ùšïñg Màrkdïg.···!!] + + + [!!Mérmàïd Réñdérér···!!] + + + [!!Réñdérš Mérmàïd dïàgràm çéllš wïth ïñpùt çòllàpšé òñ éxéçùté.···!!] + + + [!!Pàràmétérš Réñdérér···!!] + + + [!!Réñdérš pàràmétér défïñïtïòñš àš àñ ïñtéràçtïvé fòrm wïth tÿpé-àwàré ïñpùtš.···!!] + + + [!!Pòlÿglòt Ñòtébòòk Šérïàlïzér···!!] + + + [!!Ïmpòrt-òñlÿ šérïàlïzér fòr Pòlÿglòt Ñòtébòòkš .dïb fïléš.···!!] + + + [!!Jùpÿtér Šérïàlïzér···!!] + + + [!!Šérïàlïzér fòr Jùpÿtér .ïpÿñb ñòtébòòkš (ñbfòrmàt v4).···!!] + + + [!!Màrkdòwñ Šérïàlïzér···!!] + + + [!!Šérïàlïzér fòr Màrkdòwñ (.md) ñòtébòòkš wïth féñçéd çòdé çéllš.···!!] + + + [!!Véršò Šérïàlïzér···!!] + + + [!!Ñàtïvé .véršò fïlé fòrmàt šérïàlïzér.···!!] + + + [!!Véršò Dàrk···!!] + + + [!!Défàùlt dàrk thémé fòr Véršò ñòtébòòkš.···!!] + + + [!!Véršò Hïgh Çòñtràšt···!!] + + + [!!Hïgh-çòñtràšt àççéššïbïlïtÿ thémé wïth WÇÀG 2.1 ÀÀ çòmplïàñt çòlòr tòkéñš.···!!] + + + [!!Véršò Lïght···!!] + + + [!!Défàùlt lïght thémé fòr Véršò ñòtébòòkš.···!!] + + + [!!Çòllàpšéd···!!] + + + [!!Hïddéñ···!!] + + + [!!Òùtpùt Òñlÿ···!!] + + + [!!Vïšïblé···!!] + + \ No newline at end of file diff --git a/src/Verso/Resources/Strings.resx b/src/Verso/Resources/Strings.resx new file mode 100644 index 00000000..40873a07 --- /dev/null +++ b/src/Verso/Resources/Strings.resx @@ -0,0 +1,975 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Clear Cell Output + Name of the built-in extension that clears one cell's output, as listed in the Extensions panel. + + + Clears the output of the selected cell. + Second line of the Clear Output entry in the Extensions panel. + + + Clear Output + Cell toolbar button. Shorter than the extension's name because it sits on a cell, where the word 'cell' is already implied. + + + Clear Outputs + Toolbar button, and the name of the same built-in extension in the Extensions panel. + + + Clears all cell outputs in the notebook. + Second line of the Clear Outputs entry in the Extensions panel. + + + Export HTML + Name of the built-in HTML export, as listed in the Extensions panel. Its menu entry reads only 'HTML' and is not translated. + + + Exports the notebook as a self-contained HTML document. + Second line of the Export HTML entry in the Extensions panel. + + + Export Markdown + Name of the built-in Markdown export, as listed in the Extensions panel. Its menu entry reads only 'Markdown' and is not translated. + + + Exports the notebook as a Markdown document. + Second line of the Export Markdown entry in the Extensions panel. + + + Export Verso + Name of the built-in .verso export, as listed in the Extensions panel. Verso is a product name. + + + Exports a Markdown-backed notebook as a native .verso file. + Second line of the Export Verso entry in the Extensions panel. + + + Restart Kernel + Toolbar button, and the name of the same built-in extension in the Extensions panel. + + + Restarting the kernel discards all variables and execution state. Restart now? + Confirmation dialog shown before the kernel restarts, because the effect cannot be undone. + + + Restarts the active language kernel. + Second line of the Restart Kernel entry in the Extensions panel. + + + Run All + The toolbar's primary button, and the name of the same built-in extension in the Extensions panel. Keep it short: it sits next to the kernel status. + + + Executes all cells in the notebook. + Second line of the Run All entry in the Extensions panel. + + + Run Cell + Cell toolbar button, and the name of the same built-in extension in the Extensions panel. + + + Executes the selected cells. + Second line of the Run Cell entry in the Extensions panel. + + + Switch Layout + Toolbar button, and the name of the same built-in extension in the Extensions panel. + + + Cycles between available layout engines. + Second line of the Switch Layout entry in the Extensions panel. + + + Switch Theme + Toolbar button, and the name of the same built-in extension in the Extensions panel. + + + Cycles between available themes. + Second line of the Switch Theme entry in the Extensions panel. + + + Code + The kind of cell that holds code. Offered wherever a new cell can be added. + + + HTML Cell Type + Name of the built-in HTML cell type, as listed in the Extensions panel. Its entry in the new-cell list reads only 'HTML' and is not translated. + + + HTML cell type for authoring raw HTML with @variable substitution. + Second line of the HTML cell type entry in the Extensions panel. '@variable' is typed by the reader and stays as written. + + + Markdown Cell Type + Name of the built-in Markdown cell type, as listed in the Extensions panel. + + + Markdown prose cells rendered to HTML with Markdig. + Second line of the Markdown cell type entry in the Extensions panel. Markdig is a library name. + + + Mermaid Cell Type + Name of the built-in Mermaid cell type, as listed in the Extensions panel. + + + Mermaid cell type for creating diagrams with mermaid.js syntax. + Second line of the Mermaid cell type entry in the Extensions panel. mermaid.js is a library name. + + + Parameters Cell Type + Name of the built-in parameters cell type, as listed in the Extensions panel. + + + Displays and manages notebook parameter definitions as an interactive form. + Second line of the parameters cell type entry in the Extensions panel. + + + Parameters + The parameters cell as offered in the new-cell list. + + + cell + Why the consent dialog is asking: a cell in the notebook asked for this package. Appears beside the package name, in brackets. + + + local file: {0} + Why the consent dialog is asking: the package was read from a file on this machine. {0} is the file name. Appears beside the package name, in brackets. + + + marketplace + Why the consent dialog is asking: the package was chosen from the Extensions panel's search. Appears beside the package name, in brackets. + + + notebook required extensions + Why the consent dialog is asking: the notebook declares this package as one it needs. Appears beside the package name, in brackets. + + + Polyglot Notebook .dib export is not supported. Use the Verso native format. + Saving to .dib is not offered. The file extensions stay as written. + + + Extension assembly not found: {0} + {0} is the full path that was looked at. + + + Extension directory not found: {0} + {0} is the full path that was looked at. + + + Interactive input is not supported by this host. + A cell asked the reader a question somewhere that cannot ask one, such as a batch run. + + + Failed to parse Jupyter notebook. + The file opened but is not a notebook Verso can read. + + + Layout '{0}' not found. + The notebook asks to be arranged by a layout nothing installed provides. {0} is its id. + + + No kernel ID specified and no default kernel is configured. + A cell was run with nothing to run it. Shown as the cell's error. + + + No kernel registered for language '{0}'. + The notebook has a cell in a language nothing installed can run. {0} is a language id such as python, which stays as written. + + + Failed to download package '{0}' v{1}. + {0} is a package name, {1} a version number. + + + Theme '{0}' not found. + The notebook asks for a theme nothing installed provides. {0} is its id. + + + Failed to deserialize .verso document. + The file opened but is not a notebook Verso can read. + + + ... ({0} more line) + Written under output that was cut short in an exported document, for exactly one line. {0} is how many are missing. + + + ... ({0} more lines) + The same for any other number. + + + Collection Formatter + Name of the built-in collection formatter, as listed in the Extensions panel. + + + Formats collections as HTML tables. + What this formatter does, shown wherever extensions are listed. + + + Exception Formatter + Name of the built-in exception formatter, as listed in the Extensions panel. + + + Formats exceptions as structured HTML. + What this formatter does, shown wherever extensions are listed. + + + HTML Formatter + Name of the built-in HTML formatter, as listed in the Extensions panel. + + + Formats objects with a ToHtml() method as HTML. + What this formatter does, shown wherever extensions are listed. ToHtml() is written in code and stays as it is. + + + Image Formatter + Name of the built-in image formatter, as listed in the Extensions panel. + + + Formats byte arrays as inline base64 images. + What this formatter does, shown wherever extensions are listed. + + + Object Formatter + Name of the built-in object formatter, as listed in the Extensions panel. + + + Formats objects as HTML tables showing public properties and fields. + What this formatter does, shown wherever extensions are listed. + + + Primitive Formatter + Name of the built-in formatter for numbers, strings and other single values, as listed in the Extensions panel. + + + Formats primitive and common value types as plain text. + What this formatter does, shown wherever extensions are listed. + + + SVG Formatter + Name of the built-in SVG formatter, as listed in the Extensions panel. + + + Formats SVG strings as inline HTML. + What this formatter does, shown wherever extensions are listed. + + + C# language kernel powered by Roslyn scripting. + What the C# kernel is, shown wherever kernels are listed. + + + HTML Kernel + Name of the built-in HTML kernel, as listed in the Extensions panel. + + + Executes HTML cells with @variable substitution. + Second line of the HTML kernel entry in the Extensions panel. '@variable' is typed by the reader and stays as written. + + + Installed Packages + Heading above the list of packages a cell installed. Keep it short; it sits above a list. + + + Mermaid Kernel + Name of the built-in Mermaid kernel, as listed in the Extensions panel. + + + Executes Mermaid diagram cells with @variable substitution. + Second line of the Mermaid kernel entry in the Extensions panel. '@variable' is typed by the reader and stays as written. + + + Unresolved variable '@{0}'. No matching variable found in the variable store. + An HTML or Mermaid cell referred to a variable no cell has produced. {0} is the name it used; the @ in front is how it is written in the cell. + + + {0} Cell + Button under the last cell that appends a new one. {0} is the kind of cell, such as Code or Markdown. + + + Dashboard Layout + Name of the built-in dashboard layout, as listed in the Extensions panel. + + + Grid-based dashboard layout showing output-only cells. + Second line of the dashboard layout entry in the Extensions panel. + + + Dashboard + The dashboard layout as offered in the layout picker. + + + Drag to move + Tooltip on the handle that repositions a cell on the dashboard. + + + Add your first cell below. + Second line of the empty-notebook message, pointing at the row of buttons under it. + + + This notebook is empty + Shown in place of the cells when a notebook has none yet. + + + Insert a {0} cell here + Tooltip on a button that adds a cell between two others. {0} is the kind of cell, such as Code or Markdown. + + + Notebook Layout + Name of the built-in notebook layout, as listed in the Extensions panel. + + + Linear top-to-bottom notebook layout with live, editable cells in elevated cards. + Second line of the notebook layout entry in the Extensions panel. + + + Notebook + The notebook layout as offered in the layout picker. + + + Presentation Layout + Name of the built-in presentation layout, as listed in the Extensions panel. + + + Output-only presentation layout for consuming interactive notebooks. + Second line of the presentation layout entry in the Extensions panel. + + + Presentation + The presentation layout as offered in the layout picker. + + + Run + Tooltip on the button that runs one cell on the dashboard. Keep it to a word: the button holds only an icon. + + + About Magic Command + Name of the #!about magic command as an extension, as listed in the Extensions panel. The command word itself is typed into a cell and is not translated. + + + Displays Verso version, runtime information, and loaded extensions. + What #!about does, shown in the list of magic commands. + + + Loaded extensions: + Heading above the list #!about prints. Each extension follows on its own line. + + + No extensions loaded. + Printed by #!about in place of the list when nothing is loaded. + + + OS: {0} + A line of #!about output. {0} is the operating system description, which stays as the system reports it. + + + Runtime: {0} + A line of #!about output. {0} is the .NET runtime description, which stays as the runtime reports it. + + + Extension Magic Command + Name of the #!extension magic command as an extension, as listed in the Extensions panel. The command word itself is typed into a cell and is not translated. + + + Extension assembly '{0}' is already loaded. + Nothing was done because the same file was loaded earlier in this session. {0} is a file name. + + + Extension package '{0}' is already loaded. + Nothing was done because the same package was loaded earlier in this session. {0} is a package name. + + + Extension assembly not found: {0} + The path #!extension was given does not exist. {0} is the full path it looked at. + + + session-generated local assembly + Why the reader is being asked to approve a file: an earlier cell in this notebook built it. Shown in the approval dialog beside the file name. + + + Installs a NuGet package or loads a local assembly containing Verso extensions. + What #!extension does, shown in the list of magic commands. + + + Installed '{0}' {1} ({2} extension registered) + Written when exactly one extension came out of the package. {0} is a package name, {1} the version installed, {2} is 1. + + + Installed '{0}' {1} ({2} extensions registered) + Written for any other number. {0} is a package name, {1} the version, {2} how many. + + + Error loading extensions from '{0}': {1} + {0} is a file name, {1} is the underlying error, which arrives in English. + + + Error loading '{0}': {1} + The same as the previous message for a failure that did not come from loading extensions. {0} is a file name, {1} is the underlying error. + + + Loaded '{0}' ({1} extension registered) + Written when exactly one extension came out of the file. {0} is a file name, {1} is 1. + + + Loaded '{0}' ({1} extensions registered) + Written for any other number. {0} is a file name, {1} is how many. + + + Loading extension from '{0}'... + Printed before the work starts, so a slow load does not look like nothing happening. {0} is a file name. + + + '{0}' loaded but contains no [VersoExtension] types. The assembly is still available as a reference. + The file loaded but contributed nothing. {0} is a file name; [VersoExtension] is written in code and stays as it is. + + + '{0}' is not a valid .NET assembly. + The file exists but is not something that can be loaded. {0} is a file name. + + + Extension '{0}' was not approved. Skipping. + The reader declined the approval dialog. {0} is the extension or file name. + + + A NuGet package ID or path to a local .dll file. + What the first argument to #!extension is. + + + Optional package version (NuGet only). + What the second argument to #!extension is. + + + Failed to resolve extension package '{0}': {1} + {0} is a package name, {1} the underlying error, which arrives in English. + + + Resolving extension package '{0}'... + The same, when no version was asked for. {0} is a package name. + + + Resolving extension package '{0}' version '{1}'... + Printed before the download starts. {0} is a package name, {1} a version number. + + + Usage: #!extension <PackageId> [Version] or #!extension <path/to/assembly.dll> + Shown when #!extension is given nothing to install. Everything after 'Usage:' is typed at a keyboard and stays as written. + + + Import Magic Command + Name of the #!import magic command as an extension, as listed in the Extensions panel. The command word itself is typed into a cell and is not translated. + + + {0} cell + How many cells ran, written for exactly one. Goes into the sentence that reports what was imported. + + + {0} cells + The same for any other number, including none. + + + imported from {0} + Why the reader is being asked to approve an extension: a notebook this one imports asks for it. Shown in the approval dialog beside the extension name. {0} is a file name. + + + Imports another notebook file and executes its cells (any cell type other than markdown or raw), with optional parameter overrides. Pass --show-output to display the imported cells' output. + What #!import does, shown in the list of magic commands. --show-output is typed at a keyboard and stays as written. + + + {0} directive extracted + How many magic commands were lifted out of a source file, written for exactly one. + + + {0} directives extracted + The same for any other number. + + + {0} (execution failed) + Wraps the sentence reporting what was imported when the file did not run cleanly. {0} is that whole sentence. + + + Error importing notebook: {0} + {0} is the underlying error, which arrives in English. + + + File not found: {0} + The path #!import was given does not exist. {0} is the full path it looked at. + + + Invalid value for parameter '{0}' ({1}): {2} + A --param override the imported notebook cannot use. {0} is the parameter name, {1} the type it declares, {2} why the value does not fit. + + + Missing required parameter for imported notebook: +{0} + Written for exactly one. {0} is an indented list, one parameter per line, which is not translated. + + + Missing required parameters for imported notebook: +{0} + The same for more than one. + + + No serializer or kernel found for '{0}'. Supported formats: {1} + Nothing installed can read this kind of file. {0} is a file name, {1} a list of file extensions such as '.verso, .ipynb'. + + + Parameter override in name=value format. May be repeated. + What --param does. name=value is typed at a keyboard and stays as written. + + + Path to the notebook file to import. + What the first argument to #!import is. + + + Display the output produced by the imported cells. Off by default. + What --show-output does. + + + Imported {0} + What #!import did with a plain source file rather than a notebook. {0} is a file name. + + + Imported {0} ({1}) + {0} is a file name, {1} the count of directives already written out. + + + Imported {0} from {1} + What #!import did. {0} is a count of cells already written out, {1} is a file name. + + + Imported {0} from {1} ({2} failed) + The same when some cells did not run. {0} is a count of cells already written out, {1} is a file name, {2} how many failed. + + + #!import requires a file path. Usage: #!import <path> [--param name=value ...] + Shown when #!import is given nothing to import. Everything after 'Usage:' is typed at a keyboard and stays as written. + + + NuGet Magic Command + Name of the #!nuget magic command as an extension, as listed in the Extensions panel. The command word itself is typed into a cell and is not translated. + + + Downloads and references a NuGet package for use in subsequent code. + What #!nuget does, shown in the list of magic commands. + + + Installed '{0}', {1} + {0} is a package name, {1} the version installed. + + + The NuGet package ID. + What the first argument to #!nuget is. + + + Optional package version. + What the second argument to #!nuget is. + + + Failed to resolve NuGet package '{0}': {1} + {0} is a package name, {1} the underlying error, which arrives in English. + + + Resolving NuGet package '{0}'... + The same, when no version was asked for. {0} is a package name. + + + Resolving NuGet package '{0}' version '{1}'... + Printed before the download starts. {0} is a package name, {1} a version number. + + + Usage: #!nuget <PackageId> [Version] + Shown when #!nuget is given nothing to install. Everything after 'Usage:' is typed at a keyboard and stays as written. + + + Restart Magic Command + Name of the #!restart magic command as an extension, as listed in the Extensions panel. The command word itself is typed into a cell and is not translated. + + + Restarts the specified kernel, or the default kernel if no argument is given. + What #!restart does, shown in the list of magic commands. + + + Kernel '{0}' restarted. + {0} is a language id such as python, which is typed at a keyboard and stays as written. + + + Default kernel restarted. + The same when #!restart was given no argument. + + + The language ID of the kernel to restart. + What the argument to #!restart is. + + + Time Magic Command + Name of the #!time magic command as an extension, as listed in the Extensions panel. The command word itself is typed into a cell and is not translated. + + + Reports elapsed wall-clock time after cell execution. + What #!time does, shown in the list of magic commands. + + + Empty collection + Shown in place of the entries when a collection has none. + + + ({0} items) + How many entries a collection has. Sits beside the type name. {0} may end in a plus sign when the collection was too long to count, so it is a number written out rather than a count. + + + ({0} member) + How many properties and fields an object has, written for exactly one. Sits beside the type name. + + + ({0} members) + The same for any other number. + + + Showing {0} of more items + Shown under a collection that was cut short. {0} is how many are on screen. + + + Add Parameter + Button that opens the row for entering a new parameter. + + + Parameters applied successfully. + Shown after the values in a parameters cell have been handed to the kernel. + + + Cancel + Tooltip on the cross that abandons the new parameter. + + + Default + Column heading in the parameters table. + + + Description + Column heading in the parameters table. + + + Name + Column heading in the parameters table. + + + Required + Column heading in the parameters table. + + + Type + Column heading in the parameters table. + + + Add + Tooltip on the tick that saves the new parameter. + + + default value + Grey prompt inside the empty default-value box. Lower case, matching the other prompts in the row. + + + description + Grey prompt inside the empty description box. Lower case, matching the other prompts in the row. + + + Expected true/false, yes/no, or 1/0, got '{0}'. + The value does not fit the type the parameter declares. The six words listed are typed at a keyboard and stay as written. {0} is what was typed. + + + Expected a date in yyyy-MM-dd format, got '{0}'. + The value does not fit the type the parameter declares. yyyy-MM-dd is a date pattern and stays as written. {0} is what was typed. + + + Expected an ISO 8601 datetime value, got '{0}'. + The value does not fit the type the parameter declares. ISO 8601 is a standard's number and stays as written. {0} is what was typed. + + + Expected an integer value, got '{0}'. + The value does not fit the type the parameter declares. {0} is what was typed. + + + Expected a numeric value, got '{0}'. + The value does not fit the type the parameter declares. {0} is what was typed. + + + Invalid value. + Shown beside a parameter whose value does not fit its type. + + + name + Grey prompt inside the empty name box. Lower case, matching the other prompts in the row. + + + No parameters defined. + Shown in a parameters cell that has nothing in it yet. + + + Parameter '{0}' not found. + Shown when a parameter has been removed under the reader's feet. {0} is the parameter's name. + + + Remove parameter + Tooltip on the cross that deletes a parameter. + + + required + Checkbox beside a new parameter, marking it as one the notebook cannot run without. Lower case, matching the prompts in the same row. + + + Parameters + Heading of the parameters cell. + + + Unknown parameter type '{0}'. + The notebook declares a parameter of a type Verso does not have. {0} is the type it named. + + + Jupyter Polyglot Magic Splitter + Name of the built-in import hook that splits Polyglot Notebook language directives into separate cells, as listed in the Extensions panel. + + + Splits Polyglot Notebook language-switching directives in imported .ipynb files into separate cells. + What this import hook does, shown wherever extensions are listed. The file extension stays as written. + + + Error: {0} + Written in front of a message a cell could not carry out. {0} is the message. + + + Warning: {0} + Written in front of a message about something that worked but is worth knowing. {0} is the message. + + + Collapse input + Switch that hides a cell's source and leaves its output showing. + + + Default: {0} + Note under a cell visibility setting saying what it falls back to. {0} is one of the visibility states. + + + Display + Heading over the settings that control how a cell is drawn. + + + Input preview lines + How many lines of source a collapsed cell shows. + + + Output + Label on the control choosing how much of a cell's output is shown. + + + Full + Output setting: show all of it. + + + Hidden + Output setting: show none of it. + + + Preview + Output setting: show the first few lines. + + + Output preview lines + How many lines of output a previewed cell shows. + + + Preview style + Label on the control choosing how a preview is drawn. + + + Lines + Preview style: the first few lines, as written. + + + Visibility + Heading over the settings that control which layouts a cell appears in. + + + Cell Display Properties + Name of the built-in provider of the Display settings, as listed in the Extensions panel. + + + Provides per-cell input and output display settings. + Second line of the Cell Display Properties entry in the Extensions panel. + + + Cell Visibility Properties + Name of the built-in provider of the Visibility settings, as listed in the Extensions panel. + + + Provides per-layout cell visibility overrides in the properties panel. + Second line of the Cell Visibility Properties entry in the Extensions panel. + + + Fallback Renderer + Name of the renderer used when a cell type has no renderer of its own, as listed in the Extensions panel. + + + Fallback + Short name of the fallback renderer. + + + HTML Renderer + Name of the built-in HTML renderer, as listed in the Extensions panel. + + + Renders HTML cells with input collapse on execute. + Second line of the HTML renderer entry in the Extensions panel. + + + Markdown Renderer + Name of the built-in Markdown renderer, as listed in the Extensions panel. + + + Renders Markdown cells using Markdig. + Second line of the Markdown renderer entry in the Extensions panel. Markdig is a library name. + + + Mermaid Renderer + Name of the built-in Mermaid renderer, as listed in the Extensions panel. + + + Renders Mermaid diagram cells with input collapse on execute. + Second line of the Mermaid renderer entry in the Extensions panel. + + + Parameters Renderer + Name of the built-in parameters renderer, as listed in the Extensions panel. + + + Renders parameter definitions as an interactive form with type-aware inputs. + Second line of the parameters renderer entry in the Extensions panel. + + + Polyglot Notebook Serializer + Name of the built-in Polyglot Notebook reader, as listed in the Extensions panel. + + + Import-only serializer for Polyglot Notebooks .dib files. + What this serializer does, shown wherever extensions are listed. It reads that format but never writes it. The file extension stays as written. + + + Jupyter Serializer + Name of the built-in Jupyter serializer, as listed in the Extensions panel. + + + Serializer for Jupyter .ipynb notebooks (nbformat v4). + What this serializer does, shown wherever extensions are listed. The file extension and the format version stay as written. + + + Markdown Serializer + Name of the built-in Markdown serializer, as listed in the Extensions panel. + + + Serializer for Markdown (.md) notebooks with fenced code cells. + What this serializer does, shown wherever extensions are listed. The file extension stays as written. + + + Verso Serializer + Name of the serializer for Verso's own format, as listed in the Extensions panel. Verso is a product name. + + + Native .verso file format serializer. + What this serializer does, shown wherever extensions are listed. The file extension stays as written. + + + Verso Dark + The built-in dark theme. Verso is a product name and stays as written; the word describing the theme is translated. + + + Default dark theme for Verso notebooks. + Second line of the dark theme entry in the Extensions panel. + + + Verso High Contrast + The built-in high-contrast theme. Verso is a product name and stays as written; the words describing the theme are translated. + + + High-contrast accessibility theme with WCAG 2.1 AA compliant color tokens. + Second line of the high-contrast theme entry in the Extensions panel. WCAG 2.1 AA is the name of a standard. + + + Verso Light + The built-in light theme. Verso is a product name and stays as written; the word describing the theme is translated. + + + Default light theme for Verso notebooks. + Second line of the light theme entry in the Extensions panel. + + + Collapsed + Cell visibility state: the cell is drawn folded shut. + + + Hidden + Cell visibility state: the cell is not drawn at all. + + + Output Only + Cell visibility state: the output is drawn and the source is not. + + + Visible + Cell visibility state: the whole cell is drawn. + + \ No newline at end of file diff --git a/src/Verso/Resources/Strings.zh-Hans.resx b/src/Verso/Resources/Strings.zh-Hans.resx new file mode 100644 index 00000000..b5d86d87 --- /dev/null +++ b/src/Verso/Resources/Strings.zh-Hans.resx @@ -0,0 +1,747 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 清除单元格输出 + + + 清除所选单元格的输出。 + + + 清除输出 + + + 清除全部输出 + + + 清除笔记本中所有单元格的输出。 + + + 导出 HTML + + + 将笔记本导出为自包含的 HTML 文档。 + + + 导出 Markdown + + + 将笔记本导出为 Markdown 文档。 + + + 导出 Verso + + + 将基于 Markdown 的笔记本导出为原生 .verso 文件。 + + + 重启内核 + + + 重启内核会丢弃所有变量和执行状态。现在重启吗? + + + 重启活动的语言内核。 + + + 全部运行 + + + 执行笔记本中的所有单元格。 + + + 运行单元格 + + + 执行所选的单元格。 + + + 切换布局 + + + 在可用的布局引擎之间循环切换。 + + + 切换主题 + + + 在可用的主题之间循环切换。 + + + 代码 + + + HTML 单元格类型 + + + 用于编写带 @variable 替换的原始 HTML 的单元格类型。 + + + Markdown 单元格类型 + + + 使用 Markdig 渲染为 HTML 的 Markdown 文本单元格。 + + + Mermaid 单元格类型 + + + 使用 mermaid.js 语法创建图表的 Mermaid 单元格类型。 + + + 参数单元格类型 + + + 以交互式表单显示和管理笔记本的参数定义。 + + + 参数 + + + 单元格 + + + 本地文件:{0} + + + 扩展市场 + + + 笔记本所需的扩展 + + + 不支持导出为 Polyglot Notebook 的 .dib 格式。请使用 Verso 原生格式。 + + + 找不到扩展程序集:{0} + + + 找不到扩展目录:{0} + + + 此宿主不支持交互式输入。 + + + 无法解析 Jupyter 笔记本。 + + + 找不到布局“{0}”。 + + + 未指定内核 ID,也未配置默认内核。 + + + 没有为语言“{0}”注册内核。 + + + 下载包“{0}”v{1} 失败。 + + + 找不到主题“{0}”。 + + + 无法反序列化 .verso 文档。 + + + ...(另有 {0} 行) + + + ...(另有 {0} 行) + + + 集合格式化程序 + + + 将集合格式化为 HTML 表格。 + + + 异常格式化程序 + + + 将异常格式化为结构化的 HTML。 + + + HTML 格式化程序 + + + 将带有 ToHtml() 方法的对象格式化为 HTML。 + + + 图像格式化程序 + + + 将字节数组格式化为内联的 base64 图像。 + + + 对象格式化程序 + + + 将对象格式化为显示公共属性和字段的 HTML 表格。 + + + 基元类型格式化程序 + + + 将基元类型和常见值类型格式化为纯文本。 + + + SVG 格式化程序 + + + 将 SVG 字符串格式化为内联 HTML。 + + + 由 Roslyn 脚本驱动的 C# 语言内核。 + + + HTML 内核 + + + 执行带 @variable 替换的 HTML 单元格。 + + + 已安装的包 + + + Mermaid 内核 + + + 执行带 @variable 替换的 Mermaid 图表单元格。 + + + 无法解析变量“@{0}”。在变量存储中找不到匹配的变量。 + + + {0} 单元格 + + + 仪表板布局 + + + 基于网格、仅显示输出单元格的仪表板布局。 + + + 仪表板 + + + 拖动以移动 + + + 在下方添加第一个单元格。 + + + 此笔记本为空 + + + 在此处插入 {0} 单元格 + + + 笔记本布局 + + + 自上而下的线性笔记本布局,单元格实时可编辑,置于浮起的卡片中。 + + + 笔记本 + + + 演示布局 + + + 仅显示输出的演示布局,用于查看交互式笔记本。 + + + 演示 + + + 运行 + + + About 魔法命令 + + + 显示 Verso 版本、运行时信息和已加载的扩展。 + + + 已加载的扩展: + + + 未加载任何扩展。 + + + 操作系统:{0} + + + 运行时:{0} + + + Extension 魔法命令 + + + 扩展程序集“{0}”已加载。 + + + 扩展包“{0}”已加载。 + + + 找不到扩展程序集:{0} + + + 会话生成的本地程序集 + + + 安装 NuGet 包或加载包含 Verso 扩展的本地程序集。 + + + 已安装“{0}”{1}(注册了 {2} 个扩展) + + + 已安装“{0}”{1}(注册了 {2} 个扩展) + + + 从“{0}”加载扩展时出错:{1} + + + 加载“{0}”时出错:{1} + + + 已加载“{0}”(注册了 {1} 个扩展) + + + 已加载“{0}”(注册了 {1} 个扩展) + + + 正在从“{0}”加载扩展... + + + “{0}”已加载,但不包含任何 [VersoExtension] 类型。该程序集仍可作为引用使用。 + + + “{0}”不是有效的 .NET 程序集。 + + + 扩展“{0}”未获批准。正在跳过。 + + + NuGet 包 ID,或本地 .dll 文件的路径。 + + + 可选的包版本(仅限 NuGet)。 + + + 无法解析扩展包“{0}”:{1} + + + 正在解析扩展包“{0}”... + + + 正在解析扩展包“{0}”的版本“{1}”... + + + 用法:#!extension <PackageId> [Version] 或 #!extension <path/to/assembly.dll> + + + Import 魔法命令 + + + {0} 个单元格 + + + {0} 个单元格 + + + 从 {0} 导入 + + + 导入另一个笔记本文件并执行其中的单元格(markdown 和 raw 之外的任何单元格类型),可选择覆盖参数。传入 --show-output 可显示导入单元格的输出。 + + + 提取了 {0} 条指令 + + + 提取了 {0} 条指令 + + + {0}(执行失败) + + + 导入笔记本时出错:{0} + + + 找不到文件:{0} + + + 参数“{0}”({1})的值无效:{2} + + + 导入的笔记本缺少必需的参数: +{0} + + + 导入的笔记本缺少必需的参数: +{0} + + + 找不到用于“{0}”的序列化程序或内核。支持的格式:{1} + + + 以 name=value 格式覆盖参数。可重复使用。 + + + 要导入的笔记本文件的路径。 + + + 显示导入单元格产生的输出。默认关闭。 + + + 已导入 {0} + + + 已导入 {0}({1}) + + + 已从 {1} 导入 {0} + + + 已从 {1} 导入 {0}({2} 个失败) + + + #!import 需要文件路径。用法:#!import <path> [--param name=value ...] + + + NuGet 魔法命令 + + + 下载并引用 NuGet 包,以供后续代码使用。 + + + 已安装“{0}”,{1} + + + NuGet 包 ID。 + + + 可选的包版本。 + + + 无法解析 NuGet 包“{0}”:{1} + + + 正在解析 NuGet 包“{0}”... + + + 正在解析 NuGet 包“{0}”的版本“{1}”... + + + 用法:#!nuget <PackageId> [Version] + + + Restart 魔法命令 + + + 重启指定的内核;未提供参数时重启默认内核。 + + + 内核“{0}”已重启。 + + + 默认内核已重启。 + + + 要重启的内核的语言 ID。 + + + Time 魔法命令 + + + 在单元格执行后报告经过的实际时间。 + + + 空集合 + + + ({0} 项) + + + ({0} 个成员) + + + ({0} 个成员) + + + 正在显示 {0} 项,还有更多 + + + 添加参数 + + + 参数已成功应用。 + + + 取消 + + + 默认值 + + + 说明 + + + 名称 + + + 必需 + + + 类型 + + + 添加 + + + 默认值 + + + 说明 + + + 应为 true/false、yes/no 或 1/0,实际为“{0}”。 + + + 应为 yyyy-MM-dd 格式的日期,实际为“{0}”。 + + + 应为 ISO 8601 日期时间值,实际为“{0}”。 + + + 应为整数值,实际为“{0}”。 + + + 应为数值,实际为“{0}”。 + + + 值无效。 + + + 名称 + + + 未定义任何参数。 + + + 找不到参数“{0}”。 + + + 移除参数 + + + 必需 + + + 参数 + + + 未知的参数类型“{0}”。 + + + Jupyter Polyglot 魔法命令拆分器 + + + 将导入的 .ipynb 文件中 Polyglot Notebook 的语言切换指令拆分为独立的单元格。 + + + 错误:{0} + + + 警告:{0} + + + 折叠输入 + + + 默认值:{0} + + + 显示 + + + 输入预览行数 + + + 输出 + + + 完整 + + + 隐藏 + + + 预览 + + + 输出预览行数 + + + 预览样式 + + + + + + 可见性 + + + 单元格显示属性 + + + 提供每个单元格的输入和输出显示设置。 + + + 单元格可见性属性 + + + 在属性面板中提供每个布局的单元格可见性覆盖。 + + + 后备渲染器 + + + 后备 + + + HTML 渲染器 + + + 渲染 HTML 单元格,执行时折叠输入。 + + + Markdown 渲染器 + + + 使用 Markdig 渲染 Markdown 单元格。 + + + Mermaid 渲染器 + + + 渲染 Mermaid 图表单元格,执行时折叠输入。 + + + 参数渲染器 + + + 将参数定义渲染为带类型感知输入控件的交互式表单。 + + + Polyglot Notebook 序列化程序 + + + 仅用于导入 Polyglot Notebooks 的 .dib 文件的序列化程序。 + + + Jupyter 序列化程序 + + + 适用于 Jupyter .ipynb 笔记本(nbformat v4)的序列化程序。 + + + Markdown 序列化程序 + + + 适用于 Markdown (.md) 笔记本的序列化程序,围栏代码块即单元格。 + + + Verso 序列化程序 + + + Verso 原生 .verso 文件格式的序列化程序。 + + + Verso 深色 + + + Verso 笔记本的默认深色主题。 + + + Verso 高对比度 + + + 高对比度辅助功能主题,颜色标记符合 WCAG 2.1 AA。 + + + Verso 浅色 + + + Verso 笔记本的默认浅色主题。 + + + 已折叠 + + + 隐藏 + + + 仅输出 + + + 可见 + + \ No newline at end of file diff --git a/src/Verso/Scaffold.cs b/src/Verso/Scaffold.cs index 18b425ad..026dcc62 100644 --- a/src/Verso/Scaffold.cs +++ b/src/Verso/Scaffold.cs @@ -4,6 +4,7 @@ using Verso.Execution; using Verso.Extensions; using Verso.Stubs; +using Verso.Resources; namespace Verso; @@ -391,10 +392,10 @@ public async Task RestartKernelAsync(string? kernelId = null) } var id = kernelId ?? _notebook.DefaultKernelId - ?? throw new InvalidOperationException("No kernel ID specified and no default kernel is configured."); + ?? throw new InvalidOperationException(Strings.Error_NoKernelConfigured); var kernel = ResolveKernel(id) - ?? throw new InvalidOperationException($"No kernel registered for language '{id}'."); + ?? throw new InvalidOperationException(string.Format(Strings.Error_NoKernelForLanguage, id)); OnKernelRestarting?.Invoke(id); try diff --git a/src/Verso/Serializers/DibSerializer.cs b/src/Verso/Serializers/DibSerializer.cs index 4eafa76d..30daf172 100644 --- a/src/Verso/Serializers/DibSerializer.cs +++ b/src/Verso/Serializers/DibSerializer.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.RegularExpressions; using Verso.Abstractions; +using Verso.Resources; namespace Verso.Serializers; @@ -17,10 +18,10 @@ public sealed class DibSerializer : INotebookSerializer // --- IExtension --- public string ExtensionId => "verso.serializer.dib"; - public string Name => "Polyglot Notebook Serializer"; + public string Name => Strings.Serializer_Dib; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Import-only serializer for Polyglot Notebooks .dib files."; + public string? Description => Strings.Serializer_Dib_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -38,7 +39,7 @@ public bool CanImport(string filePath) public Task SerializeAsync(NotebookModel notebook) { - throw new NotSupportedException("Polyglot Notebook .dib export is not supported. Use the Verso native format."); + throw new NotSupportedException(Strings.Error_DibExportUnsupported); } public Task DeserializeAsync(string content) diff --git a/src/Verso/Serializers/JupyterPolyglotPostProcessor.cs b/src/Verso/Serializers/JupyterPolyglotPostProcessor.cs index 010bd878..421a7f90 100644 --- a/src/Verso/Serializers/JupyterPolyglotPostProcessor.cs +++ b/src/Verso/Serializers/JupyterPolyglotPostProcessor.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; using Verso.Abstractions; +using Verso.Resources; namespace Verso.Serializers; @@ -58,11 +59,10 @@ public sealed class JupyterPolyglotPostProcessor : INotebookPostProcessor // --- IExtension --- public string ExtensionId => "verso.serializer.jupyter-polyglot"; - string IExtension.Name => "Jupyter Polyglot Magic Splitter"; + string IExtension.Name => Strings.PostProcessor_JupyterPolyglot; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => - "Splits Polyglot Notebook language-switching directives in imported .ipynb files into separate cells."; + public string? Description => Strings.PostProcessor_JupyterPolyglot_Description; // --- INotebookPostProcessor --- diff --git a/src/Verso/Serializers/JupyterSerializer.cs b/src/Verso/Serializers/JupyterSerializer.cs index e126e03b..2daf2c58 100644 --- a/src/Verso/Serializers/JupyterSerializer.cs +++ b/src/Verso/Serializers/JupyterSerializer.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Verso.Abstractions; +using Verso.Resources; namespace Verso.Serializers; @@ -24,10 +25,10 @@ public sealed class JupyterSerializer : INotebookSerializer // --- IExtension --- public string ExtensionId => "verso.serializer.jupyter"; - public string Name => "Jupyter Serializer"; + public string Name => Strings.Serializer_Jupyter; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Serializer for Jupyter .ipynb notebooks (nbformat v4)."; + public string? Description => Strings.Serializer_Jupyter_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; @@ -63,7 +64,7 @@ public Task DeserializeAsync(string content) ArgumentNullException.ThrowIfNull(content); var jupyterDoc = JsonSerializer.Deserialize(content, ReadOptions) - ?? throw new JsonException("Failed to parse Jupyter notebook."); + ?? throw new JsonException(Strings.Error_JupyterParseFailed); if (jupyterDoc.NbFormat < 4) throw new NotSupportedException( diff --git a/src/Verso/Serializers/MarkdownSerializer.cs b/src/Verso/Serializers/MarkdownSerializer.cs index d146073d..c2678f10 100644 --- a/src/Verso/Serializers/MarkdownSerializer.cs +++ b/src/Verso/Serializers/MarkdownSerializer.cs @@ -1,6 +1,7 @@ using Markdig; using Markdig.Syntax; using Verso.Abstractions; +using Verso.Resources; namespace Verso.Serializers; @@ -29,10 +30,10 @@ public sealed class MarkdownSerializer : INotebookSerializer // --- IExtension --- public string ExtensionId => "verso.serializer.markdown"; - public string Name => "Markdown Serializer"; + public string Name => Strings.Serializer_Markdown; public string Version => "1.0.0"; public string? Author => "Verso Contributors"; - public string? Description => "Serializer for Markdown (.md) notebooks with fenced code cells."; + public string? Description => Strings.Serializer_Markdown_Description; public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask; public Task OnUnloadedAsync() => Task.CompletedTask; diff --git a/src/Verso/Serializers/VersoSerializer.cs b/src/Verso/Serializers/VersoSerializer.cs index af6b81ae..0290a4af 100644 --- a/src/Verso/Serializers/VersoSerializer.cs +++ b/src/Verso/Serializers/VersoSerializer.cs @@ -2,6 +2,7 @@ using System.Text.Json.Serialization; using Verso.Abstractions; using Verso.Serializers.Migrations; +using Verso.Resources; namespace Verso.Serializers; @@ -57,10 +58,10 @@ public VersoSerializer(IReadOnlyList? cellTypes) : this(NotebookMigra // --- IExtension --- public string ExtensionId => "verso.serializer.verso"; - public string Name => "Verso Serializer"; + public string Name => Strings.Serializer_Verso; public string Version => "1.0.0"; public string? Author => "Datafication"; - public string? Description => "Native .verso file format serializer."; + public string? Description => Strings.Serializer_Verso_Description; public Task OnLoadedAsync(IExtensionHostContext context) { @@ -155,7 +156,7 @@ public Task DeserializeAsync(string content) ArgumentNullException.ThrowIfNull(content); var doc = JsonSerializer.Deserialize(content, ReadOptions) - ?? throw new JsonException("Failed to deserialize .verso document."); + ?? throw new JsonException(Strings.Error_VersoParseFailed); var (activeLayout, requiresLegacyResolution) = DeserializeActiveLayout(doc.Metadata?.ActiveLayout); diff --git a/src/Verso/ThemeEngine.cs b/src/Verso/ThemeEngine.cs index 3e7084f8..19d91c95 100644 --- a/src/Verso/ThemeEngine.cs +++ b/src/Verso/ThemeEngine.cs @@ -1,5 +1,6 @@ using System.Reflection; using Verso.Abstractions; +using Verso.Resources; namespace Verso; @@ -64,7 +65,7 @@ public void SetActiveTheme(string themeId) ArgumentNullException.ThrowIfNull(themeId); var theme = _availableThemes.FirstOrDefault( t => string.Equals(t.ThemeId, themeId, StringComparison.OrdinalIgnoreCase)) - ?? throw new InvalidOperationException($"Theme '{themeId}' not found."); + ?? throw new InvalidOperationException(string.Format(Strings.Error_ThemeNotFound, themeId)); _activeTheme = theme; OnThemeChanged?.Invoke(theme); } diff --git a/src/Verso/Verso.csproj b/src/Verso/Verso.csproj index e8fa4e85..0dd17dda 100644 --- a/src/Verso/Verso.csproj +++ b/src/Verso/Verso.csproj @@ -17,8 +17,31 @@ + + + + MSBuild:Compile + $(IntermediateOutputPath)Strings.Designer.cs + CSharp + Verso.Resources + Strings + + + + + + diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 00000000..d16948b6 --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/tests/EnglishTestCulture.cs b/tests/EnglishTestCulture.cs new file mode 100644 index 00000000..63ad1de1 --- /dev/null +++ b/tests/EnglishTestCulture.cs @@ -0,0 +1,37 @@ +using System.Globalization; +// Written out rather than relied on: most test projects import this namespace globally, but +// not all of them, and this file is compiled into every one. +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Verso.Tests.Shared; + +/// +/// Pins every test run to English. +/// +/// +/// Assertions are written against the English wording of the interface, and a test process +/// otherwise takes its language from the machine it runs on. Without this, the suite passes in +/// London and fails in Berlin, which is a difference nobody would think to look for. +/// +/// Only the interface language is pinned. Formatting is left alone, so a test that depends on +/// how a number or a date is written still fails on a machine where that differs, which is a +/// real difference worth catching rather than hiding. +/// +/// +/// This file is compiled into every test assembly by tests/Directory.Build.props. Each +/// assembly needs its own copy because the hook runs once per assembly, not once per run. +/// +/// +[TestClass] +public static class EnglishTestCulture +{ + // Fully qualified, because bUnit brings a TestContext of its own into scope in the + // component test assembly and the two names collide. + [AssemblyInitialize] + public static void Pin(Microsoft.VisualStudio.TestTools.UnitTesting.TestContext context) + { + _ = context; + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo("en"); + CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en"); + } +} diff --git a/tests/Verso.Abstractions.Tests/Theming/ThemeCssTests.cs b/tests/Verso.Abstractions.Tests/Theming/ThemeCssTests.cs new file mode 100644 index 00000000..7acd0197 --- /dev/null +++ b/tests/Verso.Abstractions.Tests/Theming/ThemeCssTests.cs @@ -0,0 +1,131 @@ +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Verso.Abstractions.Tests.Theming; + +[TestClass] +public class ThemeCssTests +{ + /// + /// Runs with the formatting culture set to one that writes decimals + /// with a comma, then puts the original back. + /// + /// + /// Only the formatting culture moves. The interface language is pinned to English for the + /// whole run, and this is not about language. + /// + private static void InGerman(Action body) + { + var original = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE"); + try + { + body(); + } + finally + { + Thread.CurrentThread.CurrentCulture = original; + } + } + + [TestMethod] + public void EveryNumericToken_IsWrittenTheWayCssReadsThem() + { + // A stylesheet is not written in anyone's language: a length is always "1.4", never + // "1,4", whoever is reading it. The editor boots the notebook interface with a culture, + // and that moves how numbers are written as well as which language the interface is in, + // so without this every notebook and every exported document in a comma-decimal + // language loses its line heights, font sizes and spacing, because a browser discards + // a declaration it cannot parse. + // + // This asserts over the whole block rather than a few named tokens, so a token added + // later is covered without anyone remembering to come back here. + InGerman(() => + { + var css = ThemeCss.BuildRootBlock( + new ThemeColorTokens(), + new ThemeTypography { FontSizeBase = 14.5 }, + new ThemeSpacing(), + new ThemeElevation()); + + var offenders = Regex + .Matches(css, @"^\s*(--verso-[a-z0-9-]+):\s*(.+);$", RegexOptions.Multiline) + .Where(m => Regex.IsMatch(m.Groups[2].Value, @"^-?\d+,\d")) + .Select(m => m.Groups[0].Value.Trim()) + .ToList(); + + Assert.AreEqual( + 0, + offenders.Count, + "These tokens were written with a decimal comma, which CSS cannot parse: " + + string.Join(" | ", offenders)); + }); + } + + [TestMethod] + public void NumericTokens_KeepTheirValue() + { + // The guard above only proves nothing carries a comma. This proves the numbers are + // still the right numbers, so replacing them with something like "0" would not pass. + InGerman(() => + { + var css = ThemeCss.BuildRootBlock( + new ThemeColorTokens(), + new ThemeTypography { FontSizeBase = 14.5 }, + new ThemeSpacing(), + new ThemeElevation()); + + StringAssert.Contains(css, "--verso-font-size-base: 14.5px;"); + StringAssert.Contains(css, "--verso-editor-font-line-height: 1.4;"); + }); + } + + [TestMethod] + public void Typography_EmitsTheTokensThatAreNotFonts() + { + // The export used to reflect only over FontDescriptor properties, so these three were + // absent from every exported document while the live interface had them. + var css = ThemeCss.BuildRootBlock(null, new ThemeTypography(), null, null); + + StringAssert.Contains(css, "--verso-font-family-mono:"); + StringAssert.Contains(css, "--verso-font-family-sans:"); + StringAssert.Contains(css, "--verso-font-size-base:"); + } + + [TestMethod] + public void NullTheme_FallsBackToTheDefaultTokens() + { + var css = ThemeCss.BuildRootBlock((ITheme?)null); + + StringAssert.StartsWith(css, ":root {"); + StringAssert.Contains(css, "--verso-editor-font-family: Cascadia Code;"); + StringAssert.Contains(css, "--verso-elevation-0: none;"); + } + + [TestMethod] + public void ElevationTokens_DropTheLevelPrefix() + { + var css = ThemeCss.BuildRootBlock(null, null, null, new ThemeElevation()); + + StringAssert.Contains(css, "--verso-elevation-1:"); + Assert.IsFalse(css.Contains("--verso-elevation-level", StringComparison.Ordinal)); + } + + private static string BuildRootBlockOverload(ThemeTypography typography) => + ThemeCss.BuildRootBlock(null, typography, null, null); + + [TestMethod] + public void BuildRootBlock_IsStableAcrossCultures() + { + // The same tokens in and the same text out, whatever the machine is set to. This is the + // property the export depends on: `verso export` never pins the formatting culture, so + // it runs in whatever the operating system says. + var typography = new ThemeTypography { FontSizeBase = 14.5 }; + + var invariant = BuildRootBlockOverload(typography); + string german = ""; + InGerman(() => german = BuildRootBlockOverload(typography)); + + Assert.AreEqual(invariant, german); + } +} diff --git a/tests/Verso.Ado.Tests/Formatters/ResultSetFormatterTests.cs b/tests/Verso.Ado.Tests/Formatters/ResultSetFormatterTests.cs index 4936f559..3220f228 100644 --- a/tests/Verso.Ado.Tests/Formatters/ResultSetFormatterTests.cs +++ b/tests/Verso.Ado.Tests/Formatters/ResultSetFormatterTests.cs @@ -157,7 +157,11 @@ public void FormatResultSetHtml_Empty_ShowsNoRowsMessage() public void FormatNonQueryHtml_ShowsRowsAffected() { var html = ResultSetFormatter.FormatNonQueryHtml(5, 42, null); - Assert.IsTrue(html.Contains("5 row(s) affected")); + Assert.IsTrue(html.Contains("5 rows affected")); Assert.IsTrue(html.Contains("42 ms")); + + // One row takes the singular. The count is written from two entries rather than with + // a bracketed s, which is a shape no other language can copy. + Assert.IsTrue(ResultSetFormatter.FormatNonQueryHtml(1, 42, null).Contains("1 row affected")); } } diff --git a/tests/Verso.Ado.Tests/Integration/SqlIntegrationTests.cs b/tests/Verso.Ado.Tests/Integration/SqlIntegrationTests.cs index 22ae3945..f209465b 100644 --- a/tests/Verso.Ado.Tests/Integration/SqlIntegrationTests.cs +++ b/tests/Verso.Ado.Tests/Integration/SqlIntegrationTests.cs @@ -298,7 +298,7 @@ public async Task EndToEnd_NonQuery_ReturnsHtmlRowsAffected() Assert.IsFalse(outputs.Any(o => o.IsError)); var htmlOutput = outputs.FirstOrDefault(o => o.MimeType == "text/html"); Assert.IsNotNull(htmlOutput); - Assert.IsTrue(htmlOutput!.Content.Contains("row(s) affected")); + Assert.IsTrue(htmlOutput!.Content.Contains("1 row affected")); Assert.IsTrue(htmlOutput.Content.Contains("ms")); await DisposeConnectionsAsync(connections); diff --git a/tests/Verso.Ado.Tests/Kernel/SqlKernelTests.cs b/tests/Verso.Ado.Tests/Kernel/SqlKernelTests.cs index 0e5d20e9..20ec3047 100644 --- a/tests/Verso.Ado.Tests/Kernel/SqlKernelTests.cs +++ b/tests/Verso.Ado.Tests/Kernel/SqlKernelTests.cs @@ -225,7 +225,7 @@ public async Task ExecuteAsync_NonQuery_ReturnsRowsAffected() await kernel.ExecuteAsync("CREATE TABLE T2 (X INTEGER)", ctx); var outputs = await kernel.ExecuteAsync("INSERT INTO T2 VALUES (1)", ctx); - Assert.IsTrue(outputs.Any(o => o.Content.Contains("row(s) affected"))); + Assert.IsTrue(outputs.Any(o => o.Content.Contains("1 row affected"))); } [TestMethod] @@ -255,9 +255,9 @@ public async Task ExecuteAsync_ConsecutiveNonQueries_ConsolidatesOutput() "INSERT INTO T3b VALUES (1); INSERT INTO T3b VALUES (2); INSERT INTO T3b VALUES (3)", ctx); // Three inserts should produce a single consolidated output, not three separate ones - var nonQueryOutputs = outputs.Where(o => o.Content.Contains("row(s) affected")).ToList(); + var nonQueryOutputs = outputs.Where(o => o.Content.Contains("rows affected")).ToList(); Assert.AreEqual(1, nonQueryOutputs.Count); - Assert.IsTrue(nonQueryOutputs[0].Content.Contains("3 row(s) affected")); + Assert.IsTrue(nonQueryOutputs[0].Content.Contains("3 rows affected")); Assert.IsTrue(nonQueryOutputs[0].Content.Contains("3 statements")); } diff --git a/tests/Verso.Ado.Tests/Localization/SqlTextTests.cs b/tests/Verso.Ado.Tests/Localization/SqlTextTests.cs new file mode 100644 index 00000000..eaa37336 --- /dev/null +++ b/tests/Verso.Ado.Tests/Localization/SqlTextTests.cs @@ -0,0 +1,89 @@ +using System.Globalization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Verso.Ado.Formatters; +using Verso.Ado.Kernel; +using Verso.Ado.Models; + +namespace Verso.Ado.Tests.Localization; + +/// +/// The words a SQL cell puts on screen: what the editor offers while typing, and the line under +/// a table saying what came back. +/// +[TestClass] +public class SqlTextTests +{ + private static void InPseudoLocale(Action assert) + { + var original = CultureInfo.CurrentUICulture; + CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("qps-Ploc"); + try + { + assert(); + } + finally + { + CultureInfo.CurrentUICulture = original; + } + } + + [TestMethod] + public void Keyword_IsExplainedInTheCurrentLanguage() + { + var english = SqlKernel.DescribeKeyword("SELECT"); + Assert.IsNotNull(english); + + InPseudoLocale(() => + { + // Looked up rather than held in a table, so it answers in whatever language was asked + // for rather than the one the kernel first loaded in. + Assert.AreNotEqual(english, SqlKernel.DescribeKeyword("SELECT")); + }); + + Assert.AreEqual(english, SqlKernel.DescribeKeyword("SELECT"), + "The explanation did not return to English when the language did."); + } + + [TestMethod] + public void Keyword_IsFoundHoweverItIsTyped() + { + Assert.AreEqual(SqlKernel.DescribeKeyword("SELECT"), SqlKernel.DescribeKeyword("select")); + Assert.IsNull(SqlKernel.DescribeKeyword("Bananas")); + } + + [TestMethod] + public void RowsAffected_ComesFromTwoEntriesRatherThanAStemAndAnS() + { + StringAssert.Contains(ResultSetFormatter.FormatNonQueryHtml(1, 3, null), "1 row affected"); + + // Zero takes the plural in English, which is the whole reason this is not a test for + // "more than one". + StringAssert.Contains(ResultSetFormatter.FormatNonQueryHtml(0, 3, null), "0 rows affected"); + StringAssert.Contains(ResultSetFormatter.FormatNonQueryHtml(9, 3, null), "9 rows affected"); + } + + [TestMethod] + public void RowsAffected_IsWrittenInTheCurrentLanguage() + { + var english = ResultSetFormatter.FormatNonQueryHtml(5, 42, null); + + InPseudoLocale(() => + Assert.AreNotEqual(english, ResultSetFormatter.FormatNonQueryHtml(5, 42, null))); + } + + [TestMethod] + public void PagingScript_CarriesItsSentenceRatherThanBuildingOneInTheBrowser() + { + // The line under a table is rewritten in the browser as the reader pages through, so the + // sentence goes over as a template with its placeholders intact. Assembling it there from + // words and numbers would put it beyond a translator's reach. + var columns = new[] { new SqlColumnMetadata("X", "INTEGER", typeof(int), false) }; + var rows = Enumerable.Range(0, 120).Select(i => new object?[] { i }).ToList(); + var html = ResultSetFormatter.FormatResultSetHtml( + new SqlResultSet(columns, rows, rows.Count, false), null, pageSize: 50); + + StringAssert.Contains(html, "var SHOWING="); + StringAssert.Contains(html, "{0}"); + StringAssert.Contains(html, "{2}"); + } +} diff --git a/tests/Verso.Blazor.Shared.Tests/DiffSourcesTests.cs b/tests/Verso.Blazor.Shared.Tests/DiffSourcesTests.cs new file mode 100644 index 00000000..af7bedf2 --- /dev/null +++ b/tests/Verso.Blazor.Shared.Tests/DiffSourcesTests.cs @@ -0,0 +1,108 @@ +using System.Globalization; +using Verso.Blazor.Shared.Resources; + +namespace Verso.Blazor.Shared.Tests; + +/// +/// The baselines a notebook can be compared against: what they are called, and what stays +/// the same whatever they are called. +/// +/// +/// Both hosts read these names, and in the embedded shell the editor picks the baseline +/// while the notebook names it. The editor's menus follow the language its workbench is +/// set to and the notebook follows the language its interface is set to, so what crosses +/// between them has to be the id, never the name. +/// +[TestClass] +public sealed class DiffSourcesTests +{ + private static readonly CultureInfo Pseudo = CultureInfo.GetCultureInfo("qps-Ploc"); + + private static void InPseudoLocale(Action assert) + { + var original = CultureInfo.CurrentUICulture; + CultureInfo.CurrentUICulture = Pseudo; + try + { + assert(); + } + finally + { + CultureInfo.CurrentUICulture = original; + } + } + + [TestMethod] + public void NameOf_KnowsTheFourBuiltInSources() + { + Assert.AreEqual(UI.Compare_SourceLastSaved, DiffSources.NameOf(DiffSources.LastSaved)); + Assert.AreEqual(UI.Compare_SourceGitHead, DiffSources.NameOf(DiffSources.GitHead)); + Assert.AreEqual(UI.Compare_SourceGitRef, DiffSources.NameOf(DiffSources.GitRef)); + Assert.AreEqual(UI.Compare_SourceChooseFile, DiffSources.NameOf(DiffSources.File)); + } + + [TestMethod] + public void NameOf_LeavesAnythingElseUnnamed() + { + // A host may offer a baseline of its own. Nothing here knows what to call it, and + // returning null says so rather than inventing a name for it. + Assert.IsNull(DiffSources.NameOf("someOtherBaseline")); + Assert.IsNull(DiffSources.NameOf(null)); + } + + [TestMethod] + public void NameOf_FollowsTheCurrentLanguage() + { + var english = DiffSources.NameOf(DiffSources.LastSaved); + + InPseudoLocale(() => Assert.AreNotEqual( + english, DiffSources.NameOf(DiffSources.LastSaved), + "The name was resolved once and kept, so one reader's language would reach them all.")); + + Assert.AreEqual(english, DiffSources.NameOf(DiffSources.LastSaved), + "The name did not come back after the language did."); + } + + [TestMethod] + public void Ids_AreTheSameInEveryLanguage() + { + InPseudoLocale(() => + { + Assert.AreEqual("lastSaved", DiffSources.LastSaved); + Assert.IsNotNull(DiffSources.NameOf(DiffSources.GitHead)); + }); + } + + [TestMethod] + public void ResolvedName_CarriesTheRefAndTheFileNameThrough() + { + // A branch name and a file name are the same in every language, which is why they + // travel from wherever the baseline was picked instead of being chosen here. + StringAssert.Contains( + DiffSources.ResolvedName(DiffSources.GitRef, "release/2.0"), "release/2.0"); + Assert.AreEqual( + "quarterly-report.verso", + DiffSources.ResolvedName(DiffSources.File, "quarterly-report.verso")); + } + + [TestMethod] + public void ResolvedName_FallsBackToTheWordBaseline() + { + // Said of a comparison that ran against something this build cannot name, which is + // still true of whatever it turned out to be. + Assert.AreEqual(UI.Compare_Baseline, DiffSources.ResolvedName("someOtherBaseline", null)); + Assert.AreEqual(UI.Compare_Baseline, DiffSources.ResolvedName(DiffSources.File, " ")); + } + + [TestMethod] + public void UnavailableReason_ExplainsTheTwoItKnows() + { + Assert.AreEqual(UI.Compare_NotSavedYet, DiffSources.UnavailableReason(DiffSources.LastSaved)); + Assert.AreEqual(UI.Compare_NotInGitRepo, DiffSources.UnavailableReason(DiffSources.GitHead)); + Assert.AreEqual(UI.Compare_NotInGitRepo, DiffSources.UnavailableReason(DiffSources.GitRef)); + + // Choosing a file always works, so there is nothing to explain. + Assert.IsNull(DiffSources.UnavailableReason(DiffSources.File)); + Assert.IsNull(DiffSources.UnavailableReason("someOtherBaseline")); + } +} diff --git a/tests/Verso.Blazor.Shared.Tests/ExtensionPanelTests.cs b/tests/Verso.Blazor.Shared.Tests/ExtensionPanelTests.cs index ce2b8fc9..4efcce80 100644 --- a/tests/Verso.Blazor.Shared.Tests/ExtensionPanelTests.cs +++ b/tests/Verso.Blazor.Shared.Tests/ExtensionPanelTests.cs @@ -1,3 +1,5 @@ +using Verso.Blazor.Shared.Resources; + namespace Verso.Blazor.Shared.Tests; [TestClass] @@ -35,8 +37,8 @@ public void WithExtensions_ShowsCategoryGroups() .Add(e => e.Service, _service)); // Groups should be visible (collapsed) - Assert.IsTrue(cut.Markup.Contains("Language Kernels")); - Assert.IsTrue(cut.Markup.Contains("Themes")); + Assert.IsTrue(cut.Markup.Contains(UI.Capability_LanguageKernel_Plural)); + Assert.IsTrue(cut.Markup.Contains(UI.Capability_Theme_Plural)); } [TestMethod] @@ -244,7 +246,7 @@ public void InstalledPackage_ThatRegisteredNothing_IsFlagged() var cut = RenderComponent(p => p .Add(e => e.Service, _service)); - Assert.IsTrue(cut.Markup.Contains("Adds nothing")); + Assert.IsTrue(cut.Markup.Contains(UI.Marketplace_AddsNothing)); } [TestMethod] @@ -260,7 +262,7 @@ public void InstalledPackage_WithNothingRecorded_IsNotFlagged() var cut = RenderComponent(p => p .Add(e => e.Service, _service)); - Assert.IsFalse(cut.Markup.Contains("Adds nothing")); + Assert.IsFalse(cut.Markup.Contains(UI.Marketplace_AddsNothing)); } [TestMethod] @@ -380,7 +382,7 @@ public void SearchResult_NotInstalled_ClaimsNothingAboutWhatItAdds() var cut = RenderSearch("json"); - Assert.IsFalse(cut.Markup.Contains("Adds nothing")); + Assert.IsFalse(cut.Markup.Contains(UI.Marketplace_AddsNothing)); Assert.AreEqual(0, cut.FindAll(".verso-marketplace-add").Count); } @@ -394,7 +396,7 @@ public void ContextChips_StateScopeAndSource() var chips = cut.FindAll(".verso-marketplace-context-chip").Select(e => e.TextContent.Trim()).ToList(); Assert.AreEqual(2, chips.Count); - Assert.IsTrue(chips[0].Contains("this notebook")); + Assert.IsTrue(chips[0].Contains(UI.Marketplace_ThisNotebook)); Assert.IsTrue(chips[1].Contains("nuget.org")); } diff --git a/tests/Verso.Blazor.Shared.Tests/HostPanelsCultureTests.cs b/tests/Verso.Blazor.Shared.Tests/HostPanelsCultureTests.cs new file mode 100644 index 00000000..2befc54d --- /dev/null +++ b/tests/Verso.Blazor.Shared.Tests/HostPanelsCultureTests.cs @@ -0,0 +1,61 @@ +using System.Globalization; + +namespace Verso.Blazor.Shared.Tests; + +/// +/// The built-in panel list is written in whatever language is current when it is asked for. +/// +/// +/// The list used to be a static readonly field, which is built once, the first time +/// anything touches it. On a server that is whoever opened a notebook first, and every reader +/// after them would have seen the panel rail in that person's language. +/// +[TestClass] +public sealed class HostPanelsCultureTests +{ + private static readonly CultureInfo Pseudo = CultureInfo.GetCultureInfo("qps-Ploc"); + + [TestMethod] + public void All_FollowsTheCurrentLanguage() + { + var english = HostPanels.All.Select(p => p.DisplayName).ToList(); + + var original = CultureInfo.CurrentUICulture; + CultureInfo.CurrentUICulture = Pseudo; + List pseudo; + try + { + pseudo = HostPanels.All.Select(p => p.DisplayName).ToList(); + } + finally + { + CultureInfo.CurrentUICulture = original; + } + + CollectionAssert.AreNotEqual(english, pseudo, + "The panel list was built once and kept, so it holds whichever language touched it first."); + CollectionAssert.AreEqual( + english, HostPanels.All.Select(p => p.DisplayName).ToList(), + "The names did not come back after the language did."); + } + + [TestMethod] + public void Ids_AreTheSameInEveryLanguage() + { + var english = HostPanels.All.Select(p => p.PanelId).ToList(); + + var original = CultureInfo.CurrentUICulture; + CultureInfo.CurrentUICulture = Pseudo; + try + { + CollectionAssert.AreEqual( + english, HostPanels.All.Select(p => p.PanelId).ToList(), + "Panel ids are stored and compared against, so translating one would break the lookup."); + Assert.IsNotNull(HostPanels.Find(HostPanels.Compare)); + } + finally + { + CultureInfo.CurrentUICulture = original; + } + } +} diff --git a/tests/Verso.Blazor.Shared.Tests/NotebookPageCompareTests.cs b/tests/Verso.Blazor.Shared.Tests/NotebookPageCompareTests.cs index d084fada..2cec3608 100644 --- a/tests/Verso.Blazor.Shared.Tests/NotebookPageCompareTests.cs +++ b/tests/Verso.Blazor.Shared.Tests/NotebookPageCompareTests.cs @@ -106,7 +106,11 @@ public void HostRequestedComparison_OpensThePanel() cut.Render(); Assert.AreEqual("gitHead", service.LastDiffSourceId); - Assert.AreEqual("COMPARE", cut.Find(".verso-panel-title").TextContent); + // The header is drawn in capitals by the stylesheet, so the markup holds the word + // as written rather than a copy the code has already changed the case of. + Assert.AreEqual( + Verso.Blazor.Shared.Resources.UI.Panel_Compare, + cut.Find(".verso-panel-title").TextContent); Assert.AreEqual(0, cut.FindAll(".verso-diff-overlay").Count, "A comparison from outside lands in the panel, not over the notebook."); } diff --git a/tests/Verso.Blazor.Shared.Tests/PanelDisplayNamesTests.cs b/tests/Verso.Blazor.Shared.Tests/PanelDisplayNamesTests.cs index 5191a5d8..066ee11d 100644 --- a/tests/Verso.Blazor.Shared.Tests/PanelDisplayNamesTests.cs +++ b/tests/Verso.Blazor.Shared.Tests/PanelDisplayNamesTests.cs @@ -1,26 +1,38 @@ +using Verso.Blazor.Shared.Resources; + namespace Verso.Blazor.Shared.Tests; [TestClass] public sealed class PanelDisplayNamesTests { [TestMethod] - public void For_Properties_ReturnsCellProperties() + public void For_Properties_SaysWhichProperties() + { + // The toggle reads "Properties"; the header it opens has room to say more. + Assert.AreEqual(UI.Panel_PropertiesHeading, PanelDisplayNames.For("properties")); + Assert.AreNotEqual(UI.Panel_Properties, PanelDisplayNames.For("properties")); + } + + [TestMethod] + public void For_KnownPanels_ReturnsTheirNames() { - Assert.AreEqual("CELL PROPERTIES", PanelDisplayNames.For("properties")); + Assert.AreEqual(UI.Panel_Metadata, PanelDisplayNames.For("metadata")); + Assert.AreEqual(UI.Panel_Extensions, PanelDisplayNames.For("extensions")); + Assert.AreEqual(UI.Panel_Variables, PanelDisplayNames.For("variables")); + Assert.AreEqual(UI.Panel_Settings, PanelDisplayNames.For("settings")); } [TestMethod] - public void For_KnownPanels_ReturnsUppercaseLabels() + public void For_UnknownPanel_FallsBackToTheId() { - Assert.AreEqual("METADATA", PanelDisplayNames.For("metadata")); - Assert.AreEqual("EXTENSIONS", PanelDisplayNames.For("extensions")); - Assert.AreEqual("VARIABLES", PanelDisplayNames.For("variables")); - Assert.AreEqual("SETTINGS", PanelDisplayNames.For("settings")); + Assert.AreEqual("custom-panel", PanelDisplayNames.For("custom-panel")); } [TestMethod] - public void For_UnknownPanel_FallsBackToUppercase() + public void For_LeavesCaseAlone() { - Assert.AreEqual("CUSTOM-PANEL", PanelDisplayNames.For("custom-panel")); + // Headers are drawn in capitals by the stylesheet. Doing it here would apply one + // language's case rules to every language's words. + Assert.AreEqual("Metadata", PanelDisplayNames.For("metadata")); } } diff --git a/tests/Verso.Blazor.Shared.Tests/ServerNotebookServiceDiffTests.cs b/tests/Verso.Blazor.Shared.Tests/ServerNotebookServiceDiffTests.cs index fc735646..aad7da91 100644 --- a/tests/Verso.Blazor.Shared.Tests/ServerNotebookServiceDiffTests.cs +++ b/tests/Verso.Blazor.Shared.Tests/ServerNotebookServiceDiffTests.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using Verso.Abstractions; using Verso.Blazor.Services; +using Verso.Blazor.Shared.Resources; namespace Verso.Blazor.Shared.Tests; @@ -169,7 +170,9 @@ public async Task ComputeDiffAsync_MalformedBaselineFile_ThrowsFriendlyError() var ex = await Assert.ThrowsExceptionAsync( () => service.ComputeDiffAsync("file", badPath)); - StringAssert.Contains(ex.Message, "Could not parse"); + // Against the resource rather than the English, so rewording the message does not + // break the test and translating it does not make the test pass by accident. + StringAssert.Contains(ex.Message, string.Format(UI.Compare_ParseFailed, "broken.verso", "").TrimEnd()); } private static ServerNotebookService NewService() diff --git a/tests/Verso.Blazor.Shared.Tests/ThemeProviderTests.cs b/tests/Verso.Blazor.Shared.Tests/ThemeProviderTests.cs index 084fc444..5252beb5 100644 --- a/tests/Verso.Blazor.Shared.Tests/ThemeProviderTests.cs +++ b/tests/Verso.Blazor.Shared.Tests/ThemeProviderTests.cs @@ -251,6 +251,39 @@ public void FontSizeBase_EmittedWithPxUnit() Assert.IsTrue(style.Contains("--verso-font-size-base: 16px;")); } + [TestMethod] + public void NumericTokens_AreWrittenTheWayCssReadsThem() + { + // The editor boots this app with a culture, and that moves how numbers are written as + // well as which language the interface is in. A stylesheet is not written in anyone's + // language: a length is always "1.4", never "1,4", whoever is reading it. Without this + // every notebook in German, Spanish, or any other comma-decimal language loses its + // line heights and font sizes, because the browser drops a declaration it cannot parse. + var original = System.Threading.Thread.CurrentThread.CurrentCulture; + try + { + System.Threading.Thread.CurrentThread.CurrentCulture = + new System.Globalization.CultureInfo("de-DE"); + + var themeData = new ThemeData( + new ThemeColorTokens(), + new ThemeTypography { FontSizeBase = 14.5 }, + new ThemeSpacing()); + + var cut = RenderComponent(p => p + .Add(t => t.Theme, themeData)); + + var style = cut.Find("style").TextContent; + + StringAssert.Contains(style, "--verso-font-size-base: 14.5px;"); + StringAssert.Contains(style, "--verso-editor-font-line-height: 1.4;"); + } + finally + { + System.Threading.Thread.CurrentThread.CurrentCulture = original; + } + } + private static ThemeData CreateDefaultThemeData() { return new ThemeData( diff --git a/tests/Verso.Cli.Tests/Repl/MetaCommandTextTests.cs b/tests/Verso.Cli.Tests/Repl/MetaCommandTextTests.cs new file mode 100644 index 00000000..98581838 --- /dev/null +++ b/tests/Verso.Cli.Tests/Repl/MetaCommandTextTests.cs @@ -0,0 +1,113 @@ +using System.Globalization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Verso.Cli.Repl; +using Verso.Cli.Repl.Meta; + +namespace Verso.Cli.Tests.Repl; + +/// +/// The words .help prints: the one-line summaries and the detailed help behind each name. +/// +/// +/// A meta-command exposes both as expression-bodied properties, which is what lets a running +/// process answer in the language it was asked for. Holding either in a field would read the +/// resource once and keep whatever language happened to touch it first, and nothing else in the +/// build would notice. +/// +[TestClass] +public class MetaCommandTextTests +{ + // Built the way a session builds it, so a command added later is covered without anyone + // remembering to add it here too. + private static IReadOnlyList AllCommands() + => ReplLoop.CreateDefaultRegistry().AllOrdered; + + private static void InPseudoLocale(Action assert) + { + var original = CultureInfo.CurrentUICulture; + CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("qps-Ploc"); + try + { + assert(); + } + finally + { + CultureInfo.CurrentUICulture = original; + } + } + + [TestMethod] + public void EveryCommand_HasSomethingToSayForItself() + { + foreach (var command in AllCommands()) + { + Assert.IsFalse(string.IsNullOrWhiteSpace(command.Summary), + $".{command.Name} has no summary, so .help would print a blank row for it."); + Assert.IsFalse(string.IsNullOrWhiteSpace(command.DetailedHelp), + $".{command.Name} has no detailed help, so '.help {command.Name}' would print nothing."); + } + } + + [TestMethod] + public void EveryCommand_FollowsTheCurrentLanguage() + { + var english = AllCommands().ToDictionary(c => c.Name, c => (c.Summary, c.DetailedHelp)); + + InPseudoLocale(() => + { + foreach (var command in AllCommands()) + { + var (summary, detailed) = english[command.Name]; + Assert.AreNotEqual(summary, command.Summary, + $".{command.Name} keeps its English summary whatever language is asked for."); + Assert.AreNotEqual(detailed, command.DetailedHelp, + $".{command.Name} keeps its English help whatever language is asked for."); + } + }); + + foreach (var command in AllCommands()) + { + Assert.AreEqual(english[command.Name].Summary, command.Summary, + $".{command.Name} did not return to English when the language did."); + } + } + + [TestMethod] + public void EveryCommand_LeadsItsHelpWithWhatToType() + { + // The first line is the shape of the command, so it stays out of the resource file and + // reads the same in every language. Everything after it is translated. + InPseudoLocale(() => + { + foreach (var command in AllCommands()) + { + var firstLine = command.DetailedHelp.Split('\n')[0]; + StringAssert.StartsWith(firstLine, "." + command.Name, + $"'.help {command.Name}' does not open with the command as it is typed."); + } + }); + } + + [TestMethod] + public void NamesAndAliases_AreTheSameInEveryLanguage() + { + var english = AllCommands() + .Select(c => (c.Name, Aliases: c.Aliases.ToArray())) + .ToList(); + + InPseudoLocale(() => + { + var pseudo = AllCommands() + .Select(c => (c.Name, Aliases: c.Aliases.ToArray())) + .ToList(); + + CollectionAssert.AreEqual( + english.Select(c => c.Name).ToList(), + pseudo.Select(c => c.Name).ToList(), + "A command is typed at a keyboard, so its name cannot depend on the reader's language."); + + for (var i = 0; i < english.Count; i++) + CollectionAssert.AreEqual(english[i].Aliases, pseudo[i].Aliases); + }); + } +} diff --git a/tests/Verso.Cli.Tests/Utilities/CellCountTests.cs b/tests/Verso.Cli.Tests/Utilities/CellCountTests.cs new file mode 100644 index 00000000..020f6858 --- /dev/null +++ b/tests/Verso.Cli.Tests/Utilities/CellCountTests.cs @@ -0,0 +1,51 @@ +using System.Globalization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Verso.Cli.Utilities; + +namespace Verso.Cli.Tests.Utilities; + +/// +/// Writing out how many cells something holds, which is the one count the CLI says out loud. +/// +[TestClass] +public class CellCountTests +{ + [TestMethod] + public void Describe_UsesTheSingularForExactlyOne() + { + Assert.AreEqual("1 cell", CellCount.Describe(1)); + } + + [TestMethod] + public void Describe_UsesThePluralForEverythingElse() + { + // Zero takes the plural in English, which is the whole reason this is not a test for + // "more than one". + Assert.AreEqual("0 cells", CellCount.Describe(0)); + Assert.AreEqual("2 cells", CellCount.Describe(2)); + Assert.AreEqual("57 cells", CellCount.Describe(57)); + } + + [TestMethod] + public void Describe_NeverBuildsAWordOutOfAStemAndAnS() + { + // The forms come from two separate entries, so a language whose plural is not its + // singular with a letter on the end still gets a phrase it can use. + var original = CultureInfo.CurrentUICulture; + CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("qps-Ploc"); + try + { + var one = CellCount.Describe(1); + var many = CellCount.Describe(3); + + Assert.AreNotEqual(one, many); + Assert.AreNotEqual("1 cell", one, "The phrase did not follow the reader's language."); + StringAssert.Contains(one, "1"); + StringAssert.Contains(many, "3"); + } + finally + { + CultureInfo.CurrentUICulture = original; + } + } +} diff --git a/tests/Verso.Cli.Tests/Utilities/DisplayWidthTests.cs b/tests/Verso.Cli.Tests/Utilities/DisplayWidthTests.cs new file mode 100644 index 00000000..ec7226ab --- /dev/null +++ b/tests/Verso.Cli.Tests/Utilities/DisplayWidthTests.cs @@ -0,0 +1,72 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Verso.Cli.Utilities; + +namespace Verso.Cli.Tests.Utilities; + +/// +/// Measuring text in terminal columns rather than characters, which is what keeps a table lined +/// up once its headings are Japanese or Chinese. +/// +[TestClass] +public class DisplayWidthTests +{ + [TestMethod] + public void Measure_CountsLatinTextOneColumnPerCharacter() + { + Assert.AreEqual(0, DisplayWidth.Measure("")); + Assert.AreEqual(0, DisplayWidth.Measure(null)); + Assert.AreEqual(7, DisplayWidth.Measure("Summary")); + } + + [TestMethod] + public void Measure_CountsEastAsianCharactersTwoColumnsEach() + { + // The two headings that exposed this: "Summary" and "Language" pad correctly by + // character count, their Japanese counterparts do not. + Assert.AreEqual(4, DisplayWidth.Measure("概要")); + Assert.AreEqual(4, DisplayWidth.Measure("言語")); + Assert.AreEqual(6, DisplayWidth.Measure("表示名")); + Assert.AreEqual(8, DisplayWidth.Measure("简体中文")); + } + + [TestMethod] + public void Measure_HandlesMixedText() + { + // What a cell rule actually holds: a Japanese label, a number, and a Latin kernel id. + // Two wide characters and thirteen narrow ones, which string.Length would call 15. + Assert.AreEqual(17, DisplayWidth.Measure("セル 18 (csharp) ")); + } + + [TestMethod] + public void Measure_CountsASurrogatePairOnce() + { + // U+20BB7 is one character written as two UTF-16 units, so Length says 2 and the + // terminal draws 2 columns. Getting there by the wrong route would say 4. + Assert.AreEqual(2, DisplayWidth.Measure("\U00020BB7")); + } + + [TestMethod] + public void PadRight_FillsToTheColumnCountRatherThanTheCharacterCount() + { + Assert.AreEqual("概要 ", DisplayWidth.PadRight("概要", 10)); + Assert.AreEqual("Summary ", DisplayWidth.PadRight("Summary", 10)); + } + + [TestMethod] + public void PadRight_LeavesTextThatIsAlreadyWideEnoughAlone() + { + Assert.AreEqual("概要", DisplayWidth.PadRight("概要", 4)); + Assert.AreEqual("概要", DisplayWidth.PadRight("概要", 2)); + } + + [TestMethod] + public void PaddedColumnsLineUpAcrossLanguages() + { + // The property the call sites depend on: two rows padded to the same width occupy the + // same number of columns, whatever alphabet they are in. + var japanese = DisplayWidth.PadRight("言語", 12); + var latin = DisplayWidth.PadRight("csharp", 12); + + Assert.AreEqual(DisplayWidth.Measure(japanese), DisplayWidth.Measure(latin)); + } +} diff --git a/tests/Verso.Cli.Tests/Utilities/MessagesTests.cs b/tests/Verso.Cli.Tests/Utilities/MessagesTests.cs new file mode 100644 index 00000000..3f5c89a7 --- /dev/null +++ b/tests/Verso.Cli.Tests/Utilities/MessagesTests.cs @@ -0,0 +1,108 @@ +using System.Globalization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Verso.Cli.Resources; +using Verso.Cli.Utilities; + +namespace Verso.Cli.Tests.Utilities; + +/// +/// Assembling a line out of a translated sentence, values, and the odd thing typed at a keyboard. +/// +[TestClass] +public class MessagesTests +{ + private static readonly CultureInfo Pseudo = CultureInfo.GetCultureInfo("qps-Ploc"); + + private static void InPseudoLocale(Action assert) + { + var original = CultureInfo.CurrentUICulture; + CultureInfo.CurrentUICulture = Pseudo; + try + { + assert(); + } + finally + { + CultureInfo.CurrentUICulture = original; + } + } + + [TestMethod] + public void Say_FillsInTheValues() + { + Assert.AreEqual( + "Active layout: report", + Messages.Say(Strings.Meta_Layout_Active, "report")); + } + + [TestMethod] + public void Say_EscapesASquareBracketInAValue() + { + // A file path may contain one, and the terminal writer reads a lone bracket as the start + // of a style, so an unescaped path would either vanish or throw. + var line = Messages.Say(Strings.Meta_Load_FileNotFound, "/tmp/[draft]/notes.verso"); + + StringAssert.Contains(line, "[[draft]]"); + Assert.IsFalse(line.Contains("/[draft]/"), "A bracket in the path reached the writer unescaped."); + } + + [TestMethod] + public void Say_EscapesASquareBracketInTheSentence() + { + // Not hypothetical: the sentences that name a cell by number are written with brackets + // around it, and a translator keeps the punctuation the source used. + var line = Messages.Say(Strings.Meta_Recall_OutOfRange, 99); + + StringAssert.Contains(line, "[[99]]"); + } + + [TestMethod] + public void Typed_LeavesWhatIsTypedAsWritten() + { + var line = Messages.Typed(Strings.Repl_UnsavedHint, ".save", ".load"); + + StringAssert.Contains(line, "[bold].save[/]"); + StringAssert.Contains(line, "[bold].load[/]"); + } + + [TestMethod] + public void Typed_StillCarriesTheCommandsWhenTheSentenceIsTranslated() + { + // The whole point of naming them through placeholders: a language that puts the verb + // last moves the words around the commands, and the commands go with them unchanged. + InPseudoLocale(() => + { + var line = Messages.Typed(Strings.Repl_UnsavedHint, ".save", ".load"); + + StringAssert.Contains(line, "[bold].save[/]"); + StringAssert.Contains(line, "[bold].load[/]"); + Assert.AreNotEqual( + Messages.Typed("Run {0} first, or {1} again to discard.", ".save", ".load"), + line, + "The sentence was resolved once and kept, so one reader's language would reach them all."); + }); + } + + [TestMethod] + public void Error_AndWarning_FollowTheCurrentLanguage() + { + var english = Messages.Error("something"); + + InPseudoLocale(() => + { + Assert.AreNotEqual(english, Messages.Error("something")); + StringAssert.Contains(Messages.Error("something"), "something", + "The prefix was translated but took the message with it."); + StringAssert.Contains(Messages.Warning("something"), "something"); + }); + + Assert.AreEqual(english, Messages.Error("something"), + "The English wording did not come back after the language did."); + } + + [TestMethod] + public void In_WrapsTheWholeLine() + { + Assert.AreEqual("[green]done[/]", Messages.In("green", "done")); + } +} diff --git a/tests/Verso.Python.Tests/Localization/KernelTextTests.cs b/tests/Verso.Python.Tests/Localization/KernelTextTests.cs new file mode 100644 index 00000000..feeff294 --- /dev/null +++ b/tests/Verso.Python.Tests/Localization/KernelTextTests.cs @@ -0,0 +1,73 @@ +using System.Globalization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Verso.Python.Kernel; + +namespace Verso.Python.Tests.Localization; + +/// +/// The words the Python kernel puts on screen: what it says it is, and what its settings are +/// called in the panel that shows them. +/// +[TestClass] +public class KernelTextTests +{ + private static void InPseudoLocale(Action assert) + { + var original = CultureInfo.CurrentUICulture; + CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("qps-Ploc"); + try + { + assert(); + } + finally + { + CultureInfo.CurrentUICulture = original; + } + } + + [TestMethod] + public void Kernel_DescribesItselfInTheCurrentLanguage() + { + var kernel = new PythonKernel(); + var english = kernel.Description; + + InPseudoLocale(() => Assert.AreNotEqual(english, kernel.Description)); + + Assert.AreEqual(english, kernel.Description, + "The description did not return to English when the language did."); + } + + [TestMethod] + public void Settings_AreNamedInTheCurrentLanguage() + { + // The list is built on each read for exactly this reason. Holding it in a field would + // bake in whichever language happened to be set when the kernel first loaded, and the + // settings panel would keep showing that one for the life of the process. + var kernel = new PythonKernel(); + var english = kernel.SettingDefinitions + .Select(s => (s.DisplayName, s.Description)).ToList(); + + Assert.IsTrue(english.Count > 0, "The kernel declares no settings, so this proves nothing."); + + InPseudoLocale(() => + { + var pseudo = kernel.SettingDefinitions + .Select(s => (s.DisplayName, s.Description)).ToList(); + + CollectionAssert.AreNotEqual(english, pseudo, + "The settings panel would show English on a machine set to another language."); + }); + } + + [TestMethod] + public void SettingNames_AreTheSameInEveryLanguage() + { + // A setting's name is written into the notebook file, so a notebook saved on a German + // machine has to be readable on an English one. Only what is shown beside it is translated. + var kernel = new PythonKernel(); + var english = kernel.SettingDefinitions.Select(s => s.Name).ToList(); + + InPseudoLocale(() => + CollectionAssert.AreEqual(english, kernel.SettingDefinitions.Select(s => s.Name).ToList())); + } +} diff --git a/tests/Verso.Tests/Localization/BuiltInExtensionCultureTests.cs b/tests/Verso.Tests/Localization/BuiltInExtensionCultureTests.cs new file mode 100644 index 00000000..cbb37558 --- /dev/null +++ b/tests/Verso.Tests/Localization/BuiltInExtensionCultureTests.cs @@ -0,0 +1,108 @@ +using System.Globalization; +using Verso.Abstractions; +using Verso.Extensions.CellTypes; +using Verso.Extensions.Layouts; +using Verso.Extensions.Themes; +using Verso.Extensions.ToolbarActions; + +namespace Verso.Tests.Localization; + +/// +/// The built-in extensions answer in whatever language is current when they are asked, +/// rather than in whichever one happened to be current the first time. +/// +/// +/// A server draws notebooks for several readers from one process, and each of them may have +/// asked for a different language. Anything that resolves its name once and keeps it would +/// serve the first reader's language to everybody after them, and the failure is invisible in +/// a single-language test run. The pseudo-locale is used here because it exists whether or not +/// a translator has reached these strings yet. +/// +[TestClass] +public sealed class BuiltInExtensionCultureTests +{ + private static readonly CultureInfo Pseudo = CultureInfo.GetCultureInfo("qps-Ploc"); + + private static void InPseudoLocale(Action assert) + { + var original = CultureInfo.CurrentUICulture; + CultureInfo.CurrentUICulture = Pseudo; + try + { + assert(); + } + finally + { + CultureInfo.CurrentUICulture = original; + } + } + + [TestMethod] + public void ToolbarAction_FollowsTheCurrentLanguage() + { + var action = new RunAllAction(); + var english = action.DisplayName; + + InPseudoLocale(() => Assert.AreNotEqual( + english, action.DisplayName, + "Run All resolved its label once and kept it, so a second reader would be served the first one's language.")); + + Assert.AreEqual(english, action.DisplayName, + "The label did not come back after the language did."); + } + + [TestMethod] + public void ConfirmationPrompt_FollowsTheCurrentLanguage() + { + var action = new RestartKernelAction(); + var english = action.ConfirmationPrompt; + + InPseudoLocale(() => Assert.AreNotEqual(english, action.ConfirmationPrompt)); + } + + [TestMethod] + public void ExtensionDescription_FollowsTheCurrentLanguage() + { + IExtension extension = new MarkdownCellType(); + var english = extension.Description; + + InPseudoLocale(() => Assert.AreNotEqual(english, extension.Description)); + } + + [TestMethod] + public void LayoutName_FollowsTheCurrentLanguage() + { + var layout = new NotebookLayout(); + var english = layout.DisplayName; + + InPseudoLocale(() => Assert.AreNotEqual(english, layout.DisplayName)); + } + + [TestMethod] + public void ThemeName_FollowsTheCurrentLanguage() + { + // Only the word describing the theme is a translator's to change; the product's name + // stays as written. That is a rule for whoever writes the translation, carried by the + // note on the resource, and there is nothing in the code here to hold it to. + var theme = new VersoDarkTheme(); + var english = theme.DisplayName; + + InPseudoLocale(() => Assert.AreNotEqual(english, theme.DisplayName)); + } + + [TestMethod] + public void ExportMenuEntries_NameFormatsAndStayAsWritten() + { + // These read "HTML" and "Markdown" rather than a sentence, and a format's name is the + // same in every language. Their entries in the Extensions panel are translated; these + // are not, and a translation appearing here would be the mistake. + var html = new ExportHtmlAction(); + var markdown = new ExportMarkdownAction(); + + InPseudoLocale(() => + { + Assert.AreEqual("HTML", html.DisplayName); + Assert.AreEqual("Markdown", markdown.DisplayName); + }); + } +} diff --git a/tests/Verso.Tests/Localization/CoreMessageTextTests.cs b/tests/Verso.Tests/Localization/CoreMessageTextTests.cs new file mode 100644 index 00000000..b8f14f25 --- /dev/null +++ b/tests/Verso.Tests/Localization/CoreMessageTextTests.cs @@ -0,0 +1,158 @@ +using System.Globalization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Verso.Abstractions; +using Verso.Extensions.Formatters; +using Verso.MagicCommands; +using Verso.Parameters; +using Verso.Resources; + +namespace Verso.Tests.Localization; + +/// +/// The words the engine itself puts on screen: what a magic command says it does, what a +/// formatter says it does, and what a cell is told about a value it cannot use. +/// +/// +/// Every one is an expression-bodied property, which is what lets a running host answer in the +/// language it was asked for. Holding one in a field would read its resource once and keep +/// whatever language happened to touch it first, and nothing else in the build would notice. +/// +[TestClass] +public class CoreMessageTextTests +{ + private static void InPseudoLocale(Action assert) + { + var original = CultureInfo.CurrentUICulture; + CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("qps-Ploc"); + try + { + assert(); + } + finally + { + CultureInfo.CurrentUICulture = original; + } + } + + // Built the way the extension host builds them, so anything added later is covered without + // anyone remembering to add it here too. + private static IReadOnlyList MagicCommands() => new IMagicCommand[] + { + new AboutMagicCommand(), + new ExtensionMagicCommand(), + new ImportMagicCommand(), + new NuGetMagicCommand(), + new RestartMagicCommand(), + new TimeMagicCommand(), + }; + + private static IReadOnlyList Formatters() => new IDataFormatter[] + { + new CollectionFormatter(), + new ExceptionFormatter(), + new HtmlFormatter(), + new ImageFormatter(), + new ObjectFormatter(), + new PrimitiveFormatter(), + new SvgFormatter(), + }; + + [TestMethod] + public void EveryMagicCommand_DescribesItselfInTheCurrentLanguage() + { + var english = MagicCommands().ToDictionary(c => c.Name, c => c.Description); + + InPseudoLocale(() => + { + foreach (var command in MagicCommands()) + { + Assert.AreNotEqual(english[command.Name], command.Description, + $"#!{command.Name} keeps its English description whatever language is asked for."); + } + }); + + foreach (var command in MagicCommands()) + { + Assert.AreEqual(english[command.Name], command.Description, + $"#!{command.Name} did not return to English when the language did."); + } + } + + [TestMethod] + public void EveryMagicCommand_DescribesItsArgumentsInTheCurrentLanguage() + { + // An argument's description is what tells a reader what to type there, so it follows the + // language for the same reason the command's own description does. + var english = MagicCommands() + .SelectMany(c => c.Parameters.Select(p => p.Description)) + .Where(d => d is not null) + .ToList(); + + Assert.IsTrue(english.Count > 0, "No magic command declares an argument, so this proves nothing."); + + InPseudoLocale(() => + { + var pseudo = MagicCommands() + .SelectMany(c => c.Parameters.Select(p => p.Description)) + .Where(d => d is not null) + .ToList(); + + CollectionAssert.AreNotEqual(english, pseudo); + }); + } + + [TestMethod] + public void EveryMagicCommand_KeepsItsNameInEveryLanguage() + { + var english = MagicCommands().Select(c => c.Name).ToList(); + + InPseudoLocale(() => + { + CollectionAssert.AreEqual(english, MagicCommands().Select(c => c.Name).ToList(), + "A magic command is typed at a keyboard, so its name cannot depend on the reader's language."); + }); + } + + [TestMethod] + public void EveryFormatter_DescribesItselfInTheCurrentLanguage() + { + var english = Formatters().ToDictionary(f => f.ExtensionId, f => f.Description); + + InPseudoLocale(() => + { + foreach (var formatter in Formatters()) + { + Assert.AreNotEqual(english[formatter.ExtensionId], formatter.Description, + $"{formatter.ExtensionId} keeps its English description whatever language is asked for."); + } + }); + } + + [TestMethod] + public void ParameterValue_ExplainsARejectedValueInTheCurrentLanguage() + { + Assert.IsFalse(ParameterValueParser.TryParse("int", "seven", out _, out var english)); + Assert.AreEqual("Expected an integer value, got 'seven'.", english); + + InPseudoLocale(() => + { + Assert.IsFalse(ParameterValueParser.TryParse("int", "seven", out _, out var pseudo)); + Assert.AreNotEqual(english, pseudo, "The reason did not follow the reader's language."); + StringAssert.Contains(pseudo!, "seven", "The value the reader typed was lost in translation."); + }); + } + + [TestMethod] + public void CountedThings_ComeFromTwoEntriesRatherThanAStemAndAnS() + { + Assert.AreEqual("... (1 more line)", string.Format( + Plural.Of(1, Strings.Export_MoreLines_One, Strings.Export_MoreLines_Other), 1)); + Assert.AreEqual("... (4 more lines)", string.Format( + Plural.Of(4, Strings.Export_MoreLines_One, Strings.Export_MoreLines_Other), 4)); + + // Zero takes the plural in English, which is the whole reason this is not a test for + // "more than one". + Assert.AreEqual("(0 members)", string.Format( + Plural.Of(0, Strings.ObjectTree_MemberCount_One, Strings.ObjectTree_MemberCount_Other), 0)); + } +} diff --git a/tests/Verso.Tests/Localization/VersoCulturesTests.cs b/tests/Verso.Tests/Localization/VersoCulturesTests.cs new file mode 100644 index 00000000..36fa6e5d --- /dev/null +++ b/tests/Verso.Tests/Localization/VersoCulturesTests.cs @@ -0,0 +1,165 @@ +using System.Globalization; +using Verso.Localization; + +namespace Verso.Tests.Localization; + +[TestClass] +public class VersoCulturesTests +{ + private CultureInfo _originalUiCulture = null!; + private string? _originalEnvironment; + + [TestInitialize] + public void Setup() + { + _originalUiCulture = CultureInfo.CurrentUICulture; + _originalEnvironment = Environment.GetEnvironmentVariable(VersoCultures.EnvironmentVariable); + Environment.SetEnvironmentVariable(VersoCultures.EnvironmentVariable, null); + } + + [TestCleanup] + public void Cleanup() + { + Environment.SetEnvironmentVariable(VersoCultures.EnvironmentVariable, _originalEnvironment); + CultureInfo.CurrentUICulture = _originalUiCulture; + CultureInfo.DefaultThreadCurrentUICulture = null; + } + + [TestMethod] + public void Supported_LeadsWithTheLanguageEverythingIsWrittenIn() + { + Assert.AreEqual(VersoCultures.Default, VersoCultures.Supported[0]); + } + + [TestMethod] + public void Supported_DoesNotOfferThePseudoLocale() + { + CollectionAssert.DoesNotContain(VersoCultures.Supported.ToList(), VersoCultures.Pseudo); + } + + [TestMethod] + public void TryMatch_AcceptsAShippedLanguage() + { + Assert.IsTrue(VersoCultures.TryMatch("de", out var culture)); + Assert.AreEqual("de", culture.Name); + } + + [TestMethod] + public void TryMatch_NarrowsARegionToItsLanguage() + { + Assert.IsTrue(VersoCultures.TryMatch("de-AT", out var culture)); + Assert.AreEqual("de", culture.Name); + } + + [TestMethod] + public void TryMatch_ReachesSimplifiedChineseThroughTheParentChain() + { + // zh-CN shares no prefix with zh-Hans, so a prefix comparison would miss it. This is the + // tag a browser or an editor is most likely to send for simplified Chinese. + Assert.IsTrue(VersoCultures.TryMatch("zh-CN", out var culture)); + Assert.AreEqual("zh-Hans", culture.Name); + } + + [TestMethod] + public void TryMatch_AcceptsThePseudoLocaleByName() + { + Assert.IsTrue(VersoCultures.TryMatch("qps-ploc", out var culture)); + Assert.AreEqual(VersoCultures.Pseudo, culture.Name); + } + + [TestMethod] + public void TryMatch_RejectsALanguageWithNoTranslation() + { + Assert.IsFalse(VersoCultures.TryMatch("fi", out _)); + } + + [TestMethod] + public void TryMatch_RejectsNonsense() + { + Assert.IsFalse(VersoCultures.TryMatch("not-a-language-tag", out _)); + } + + [TestMethod] + public void TryMatch_TreatsAutoAsNoAnswer() + { + Assert.IsFalse(VersoCultures.TryMatch("auto", out _)); + } + + [TestMethod] + public void TryMatch_TreatsNullAndBlankAsNoAnswer() + { + Assert.IsFalse(VersoCultures.TryMatch(null, out _)); + Assert.IsFalse(VersoCultures.TryMatch(" ", out _)); + } + + [TestMethod] + public void Resolve_PrefersTheExplicitRequest() + { + Environment.SetEnvironmentVariable(VersoCultures.EnvironmentVariable, "ja"); + CultureInfo.CurrentUICulture = new CultureInfo("es"); + + Assert.AreEqual("de", VersoCultures.Resolve("de").Name); + } + + [TestMethod] + public void Resolve_FallsBackToTheEnvironmentVariable() + { + Environment.SetEnvironmentVariable(VersoCultures.EnvironmentVariable, "ja"); + CultureInfo.CurrentUICulture = new CultureInfo("es"); + + Assert.AreEqual("ja", VersoCultures.Resolve(null).Name); + } + + [TestMethod] + public void Resolve_FallsBackToTheSystemLanguage() + { + CultureInfo.CurrentUICulture = new CultureInfo("es-MX"); + + Assert.AreEqual("es", VersoCultures.Resolve(null).Name); + } + + [TestMethod] + public void Resolve_FallsBackToEnglishWhenNothingMatches() + { + CultureInfo.CurrentUICulture = new CultureInfo("fi-FI"); + + Assert.AreEqual(VersoCultures.Default, VersoCultures.Resolve("nope").Name); + } + + [TestMethod] + public void FromArguments_ReadsASeparateValue() + { + Assert.AreEqual("de", VersoCultures.FromArguments(new[] { "serve", "--language", "de" })); + } + + [TestMethod] + public void FromArguments_ReadsAnAttachedValue() + { + Assert.AreEqual("zh-Hans", VersoCultures.FromArguments(new[] { "--language=zh-Hans", "run" })); + } + + [TestMethod] + public void FromArguments_IgnoresAnOptionWithNothingAfterIt() + { + Assert.IsNull(VersoCultures.FromArguments(new[] { "run", "--language" })); + } + + [TestMethod] + public void FromArguments_AnswersNothingWhenTheOptionIsAbsent() + { + Assert.IsNull(VersoCultures.FromArguments(new[] { "run", "notebook.verso", "--verbose" })); + } + + [TestMethod] + public void ApplyUiCulture_LeavesNumberFormattingAlone() + { + // A cell's results are data. Picking a menu language must not change how this process + // renders the numbers a kernel produced. + var before = CultureInfo.CurrentCulture.Name; + + VersoCultures.ApplyUiCulture(new CultureInfo("de")); + + Assert.AreEqual("de", CultureInfo.CurrentUICulture.Name); + Assert.AreEqual(before, CultureInfo.CurrentCulture.Name); + } +} diff --git a/vscode/.gitignore b/vscode/.gitignore index fb14b6e8..0d3413f4 100644 --- a/vscode/.gitignore +++ b/vscode/.gitignore @@ -4,3 +4,4 @@ out/ out-test/ /host/ *.vsix +.vscode-test/ diff --git a/vscode/README.md b/vscode/README.md index d45182ae..40cf9840 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -177,6 +177,12 @@ Notebooks can also be exported to HTML or Markdown from the editor's Export menu \* IntelliSense for JavaScript and TypeScript comes from Monaco's built-in language services rather than the kernel. +## Interface Language + +The notebook interface is translated into German, Spanish, Japanese, and Simplified Chinese. It follows your VS Code display language on its own, and `verso.language` overrides that for Verso alone. A change applies the next time a notebook is opened. + +Menu entries, command names, and the descriptions of these settings come from VS Code and always follow its display language, which no extension can override. So the Compare command in the Command Palette and the Compare panel inside a notebook can legitimately be showing two different languages at once. + ## Settings | Setting | Default | Description | @@ -184,6 +190,7 @@ Notebooks can also be exported to HTML or Markdown from the editor's Export menu | `verso.dotnetPath` | auto-detect | Path to the `dotnet` executable used to run notebooks. If empty, Verso reuses an installed .NET runtime, locating it via the .NET Install Tool. | | `verso.extensionsPath` | `[]` | Directories of third-party Verso extension assemblies to load on notebook open, one directory per entry. Applies on the next notebook open. | | `verso.hostPath` | bundled | Path to a custom `Verso.Host.dll`. If empty, the bundled host is used. | +| `verso.language` | `auto` | Language of the notebook interface and kernel messages: English, Deutsch, Español, 日本語, or 简体中文. `auto` follows the VS Code display language. Applies on the next notebook open. | | `verso.preserveOriginalFormat` | `false` | When opening an `.ipynb` file, save changes back to `.ipynb` (cell outputs preserved) instead of converting to a sibling `.verso` file. | | `verso.showOpenInVersoMenu` | `true` | Show the **Open as Verso Notebook** entry in the Explorer context menu for `.md` files. Turning it off hides the entry only; **Reopen Editor With...** still works. | | `verso.python.interpreterPath` | auto-detect | Path to the Python interpreter used by Python cells. If empty, Verso discovers one from the active virtual environment, the workspace, and well-known install locations. | diff --git a/vscode/l10n/bundle.l10n.de.json b/vscode/l10n/bundle.l10n.de.json new file mode 100644 index 00000000..83c53819 --- /dev/null +++ b/vscode/l10n/bundle.l10n.de.json @@ -0,0 +1,96 @@ +{ + "'{0}' is not a known branch, tag, or commit.": "'{0}' ist kein bekannter Branch, kein Tag und kein Commit.", + "'{0}' is not tracked at '{1}'. Commit the file first, or pick a different ref.": "'{0}' wird unter '{1}' nicht versioniert. Committen Sie die Datei zuerst, oder wählen Sie einen anderen Ref.", + "(not set)/Stands in a table cell for a setting that has no value yet.": "(nicht festgelegt)", + "(unnamed)/Stands in for a branch or tag that has no name to show.": "(unbenannt)", + "**Cell {0}** [{1}] properties:": "Eigenschaften von **Zelle {0}** [{1}]:", + "Adding parameter \"{0}\" ({1})": "Parameter \"{0}\" ({1}) wird hinzugefügt", + "Adding {0} cell ({1})": "{0}-Zelle wird hinzugefügt ({1})", + "All Files/The entry in a save box that accepts any file at all.": "Alle Dateien", + "Cell ({0}ms)/{0} is a number of milliseconds; ms is the unit and stays as written.": "Zelle ({0}ms)", + "Cell {0} has no configurable properties.": "Zelle {0} hat keine konfigurierbaren Eigenschaften.", + "Cell {0} not found. The notebook has {1}.": "Zelle {0} wurde nicht gefunden. Das Notebook hat {1}.", + "Cell {0}/A heading over one cell's code. {0} counts from 1.": "Zelle {0}", + "Changing cell {0} language to \"{1}\"": "Sprache von Zelle {0} wird auf \"{1}\" geändert", + "Changing cell {0} to type \"{1}\"": "Zelle {0} wird auf den Typ \"{1}\" geändert", + "Choose File.../Opens a box for picking another notebook to compare against. Keep the three dots, which mean a question follows.": "Datei auswählen...", + "Compare notebook with...": "Notebook vergleichen mit...", + "Compare/The button that accepts the chosen file, in place of \"Open\".": "Vergleichen", + "Error: {0}/{0} is what the cell reported, in the language the kernel reported it.": "Fehler: {0}", + "Export failed: {0}": "Export fehlgeschlagen: {0}", + "Exported to {0}": "Nach {0} exportiert", + "Extensions/Names the kind of file the box will accept.": "Erweiterungen", + "Git branch, tag, or commit SHA/Says what may be typed. Every term here is a version control term and stays as written.": "Git-Branch, Tag oder Commit-SHA", + "Git: Compare with Ref.../Opens a box for choosing a branch, tag, or commit. Git and Ref are version control terms and are not translated. Keep the three dots, which mean a question follows.": "Git: Mit Ref vergleichen...", + "Git: HEAD/One of the things a notebook can be compared against. Git and HEAD are version control terms and are not translated.": "Git: HEAD", + "Images/Names the kind of file a save box will accept: pictures.": "Bilder", + "Input request failed: {0}": "Die Eingabeanforderung ist fehlgeschlagen: {0}", + "Install .NET Runtime/A button. .NET is a product name and stays as written.": ".NET-Laufzeit installieren", + "Install Extension/The button that accepts the chosen file, in place of \"Open\".": "Erweiterung installieren", + "Kernel restart failed: the host process did not start ({0}). Close and reopen the notebook.": "Der Neustart des Kernels ist fehlgeschlagen: Der Hostprozess wurde nicht gestartet ({0}). Schließen Sie das Notebook und öffnen Sie es erneut.", + "Kernel restart failed: the notebook did not reopen ({0}).": "Der Neustart des Kernels ist fehlgeschlagen: Das Notebook wurde nicht erneut geöffnet ({0}).", + "Last Saved/One of the things a notebook can be compared against: the copy currently on disk.": "Zuletzt gespeichert", + "Model error: {0}": "Modellfehler: {0}", + "Moving cell {0} to position {1}": "Zelle {0} wird an Position {1} verschoben", + "Name/A table heading: what a variable is called.": "Name", + "No Verso notebook is currently open.": "Derzeit ist kein Verso-Notebook geöffnet.", + "No Verso notebook is currently open. Open a `.verso`, `.ipynb`, `.md`, or `.dib` file first.": "Derzeit ist kein Verso-Notebook geöffnet. Öffnen Sie zuerst eine `.verso`-, `.ipynb`-, `.md`- oder `.dib`-Datei.", + "No variables in scope. Run some cells first.": "Keine Variablen im Gültigkeitsbereich. Führen Sie zuerst einige Zellen aus.", + "No/A table cell answering whether a setting is read-only. The answer to a question, not the word for a number.": "Nein", + "Notebook Files/Names the kind of file the box will accept.": "Notebook-Dateien", + "Notebook input/Asked when a running cell wants something typed and did not say what.": "Notebook-Eingabe", + "Property/A table heading: the name of one setting on a cell.": "Eigenschaft", + "Provider: {0}/Names the extension a group of settings came from. {0} is its id.": "Anbieter: {0}", + "Read-only/A table heading: whether a setting can be changed.": "Schreibgeschützt", + "Remove cell": "Zelle entfernen", + "Remove cell **{0}** from the notebook?": "Zelle **{0}** aus dem Notebook entfernen?", + "Remove parameter": "Parameter entfernen", + "Remove parameter **{0}** from the notebook?": "Parameter **{0}** aus dem Notebook entfernen?", + "Removing cell {0}": "Zelle {0} wird entfernt", + "Removing parameter \"{0}\"": "Parameter \"{0}\" wird entfernt", + "Restart aborted: the notebook snapshot could not be captured ({0}).": "Der Neustart wurde abgebrochen: Die Momentaufnahme des Notebooks konnte nicht erstellt werden ({0}).", + "Running all cells": "Alle Zellen werden ausgeführt", + "Running all cells...": "Alle Zellen werden ausgeführt...", + "Running cell {0}": "Zelle {0} wird ausgeführt", + "Select a notebook for @verso/@verso is typed to address the assistant and stays as written.": "Ein Notebook für @verso auswählen", + "Setting \"{0}\" on cell {1}": "\"{0}\" wird für Zelle {1} festgelegt", + "Setup Help/A button. It opens the page describing how to set Verso up.": "Hilfe zur Einrichtung", + "Switching layout to \"{0}\"": "Layout wird auf \"{0}\" gewechselt", + "The notebook file is not inside a git repository.": "Die Notebook-Datei liegt nicht in einem git-Repository.", + "The notebook has no file on disk yet.": "Das Notebook hat noch keine Datei auf der Festplatte.", + "The notebook is empty.": "Das Notebook ist leer.", + "This comparison source is not available.": "Diese Vergleichsquelle ist nicht verfügbar.", + "This notebook is not inside a git repository.": "Dieses Notebook liegt nicht in einem git-Repository.", + "Type a ref or commit.../The last entry in a list of branches and tags, for naming one that is not listed. Ref and commit are version control terms.": "Ref oder Commit eingeben...", + "Type/A table heading: what kind of value a setting takes.": "Typ", + "Type/A table heading: what kind of value a variable holds.": "Typ", + "Unknown comparison source '{0}'.": "Unbekannte Vergleichsquelle '{0}'.", + "Updating cell {0}": "Zelle {0} wird aktualisiert", + "Updating parameter \"{0}\"": "Parameter \"{0}\" wird aktualisiert", + "Usage: `/props ` (1-based). Example: `/props 2`/Shown when /props was typed without a cell number. Everything in backticks is typed and stays as written.": "Verwendung: `/props ` (ab 1 gezählt). Beispiel: `/props 2`", + "Value/A table heading: what a setting is currently set to.": "Wert", + "Value/A table heading: what a variable currently holds.": "Wert", + "Verso host process exited ({0})": "Der Verso-Hostprozess wurde beendet ({0})", + "Verso needs the .NET runtime (version {0} or later) to run notebooks, but a compatible installation was not found.": "Verso benötigt die .NET-Laufzeit (Version {0} oder neuer), um Notebooks auszuführen, es wurde jedoch keine passende Installation gefunden.", + "Verso: Could not create a scratch notebook: {0}": "Verso: Es konnte kein Entwurfs-Notebook erstellt werden: {0}", + "Verso: Could not find Verso.Host.dll. Set \"verso.hostPath\" in settings to the path of your built Verso.Host.dll.": "Verso: Verso.Host.dll wurde nicht gefunden. Setzen Sie \"verso.hostPath\" in den Einstellungen auf den Pfad Ihrer erstellten Verso.Host.dll.", + "Verso: Failed to open notebook: {0}": "Verso: Das Notebook konnte nicht geöffnet werden: {0}", + "Verso: Failed to start host process: {0}": "Verso: Der Hostprozess konnte nicht gestartet werden: {0}", + "Verso: Open a notebook to compare it with a baseline.": "Verso: Öffnen Sie ein Notebook, um es mit einer Basis zu vergleichen.", + "Verso: installing the .NET runtime...": "Verso: Die .NET-Laufzeit wird installiert...", + "Verso: kernel restart aborted because the notebook snapshot could not be captured. Save and reopen the file.": "Verso: Der Neustart des Kernels wurde abgebrochen, weil die Momentaufnahme des Notebooks nicht erstellt werden konnte. Speichern Sie die Datei und öffnen Sie sie erneut.", + "Verso: kernel restart failed (notebook did not reopen): {0}. Close and reopen the notebook.": "Verso: Der Neustart des Kernels ist fehlgeschlagen (das Notebook wurde nicht erneut geöffnet): {0}. Schließen Sie das Notebook und öffnen Sie es erneut.", + "Verso: the .NET runtime is installed. Reopen the notebook to continue.": "Verso: Die .NET-Laufzeit ist installiert. Öffnen Sie das Notebook erneut, um fortzufahren.", + "Verso: the notebooks already open keep their current language. Reopen them to read them in the new one.": "Verso: Bereits geöffnete Notebooks behalten ihre aktuelle Sprache. Öffnen Sie sie erneut, um sie in der neuen Sprache zu lesen.", + "Yes/A table cell answering whether a setting is read-only.": "Ja", + "branch/Says what kind of thing is listed, shown beside its name. A line of work in version control.": "Branch", + "git could not read '{0}' at '{1}': {2}": "git konnte '{0}' unter '{1}' nicht lesen: {2}", + "remote branch/A branch that lives on the server rather than on this machine.": "Remote-Branch", + "tag/A name pinned to one point in a project's history.": "Tag", + "unavailable/Said of a baseline that cannot be compared against, for a reason nothing here knows.": "nicht verfügbar", + "{0} Files/Names the kind of file a save box will accept. {0} is a format name such as CSV or HTML and is the same word in every language.": "{0}-Dateien", + "{0} cell/Used when {0} is 1. Paired with the entry below.": "{0} Zelle", + "{0} cells/Used for every count other than 1. A language with one form for both translates this the same as the entry above.": "{0} Zellen", + "{0} line/Used when {0} is 1. Paired with the entry below.": "{0} Zeile", + "{0} lines/Used for every count other than 1. A language with one form for both translates this the same as the entry above.": "{0} Zeilen" +} diff --git a/vscode/l10n/bundle.l10n.es.json b/vscode/l10n/bundle.l10n.es.json new file mode 100644 index 00000000..805390a0 --- /dev/null +++ b/vscode/l10n/bundle.l10n.es.json @@ -0,0 +1,96 @@ +{ + "'{0}' is not a known branch, tag, or commit.": "'{0}' no es una rama, etiqueta ni confirmación conocida.", + "'{0}' is not tracked at '{1}'. Commit the file first, or pick a different ref.": "'{0}' no está bajo control de versiones en '{1}'. Confirme el archivo primero, o elija otra referencia.", + "(not set)/Stands in a table cell for a setting that has no value yet.": "(sin definir)", + "(unnamed)/Stands in for a branch or tag that has no name to show.": "(sin nombre)", + "**Cell {0}** [{1}] properties:": "Propiedades de la **celda {0}** [{1}]:", + "Adding parameter \"{0}\" ({1})": "Añadiendo el parámetro \"{0}\" ({1})", + "Adding {0} cell ({1})": "Añadiendo una celda {0} ({1})", + "All Files/The entry in a save box that accepts any file at all.": "Todos los archivos", + "Cell ({0}ms)/{0} is a number of milliseconds; ms is the unit and stays as written.": "Celda ({0}ms)", + "Cell {0} has no configurable properties.": "La celda {0} no tiene propiedades configurables.", + "Cell {0} not found. The notebook has {1}.": "No se encontró la celda {0}. El cuaderno tiene {1}.", + "Cell {0}/A heading over one cell's code. {0} counts from 1.": "Celda {0}", + "Changing cell {0} language to \"{1}\"": "Cambiando el lenguaje de la celda {0} a \"{1}\"", + "Changing cell {0} to type \"{1}\"": "Cambiando la celda {0} al tipo \"{1}\"", + "Choose File.../Opens a box for picking another notebook to compare against. Keep the three dots, which mean a question follows.": "Elegir archivo...", + "Compare notebook with...": "Comparar el cuaderno con...", + "Compare/The button that accepts the chosen file, in place of \"Open\".": "Comparar", + "Error: {0}/{0} is what the cell reported, in the language the kernel reported it.": "Error: {0}", + "Export failed: {0}": "La exportación falló: {0}", + "Exported to {0}": "Exportado a {0}", + "Extensions/Names the kind of file the box will accept.": "Extensiones", + "Git branch, tag, or commit SHA/Says what may be typed. Every term here is a version control term and stays as written.": "Rama, etiqueta o SHA de confirmación de Git", + "Git: Compare with Ref.../Opens a box for choosing a branch, tag, or commit. Git and Ref are version control terms and are not translated. Keep the three dots, which mean a question follows.": "Git: Comparar con Ref...", + "Git: HEAD/One of the things a notebook can be compared against. Git and HEAD are version control terms and are not translated.": "Git: HEAD", + "Images/Names the kind of file a save box will accept: pictures.": "Imágenes", + "Input request failed: {0}": "La solicitud de entrada falló: {0}", + "Install .NET Runtime/A button. .NET is a product name and stays as written.": "Instalar el runtime de .NET", + "Install Extension/The button that accepts the chosen file, in place of \"Open\".": "Instalar la extensión", + "Kernel restart failed: the host process did not start ({0}). Close and reopen the notebook.": "El reinicio del kernel falló: el proceso host no se inició ({0}). Cierre y vuelva a abrir el cuaderno.", + "Kernel restart failed: the notebook did not reopen ({0}).": "El reinicio del kernel falló: el cuaderno no se volvió a abrir ({0}).", + "Last Saved/One of the things a notebook can be compared against: the copy currently on disk.": "Último guardado", + "Model error: {0}": "Error del modelo: {0}", + "Moving cell {0} to position {1}": "Moviendo la celda {0} a la posición {1}", + "Name/A table heading: what a variable is called.": "Nombre", + "No Verso notebook is currently open.": "No hay ningún cuaderno de Verso abierto.", + "No Verso notebook is currently open. Open a `.verso`, `.ipynb`, `.md`, or `.dib` file first.": "No hay ningún cuaderno de Verso abierto. Abra antes un archivo `.verso`, `.ipynb`, `.md` o `.dib`.", + "No variables in scope. Run some cells first.": "No hay variables en ámbito. Ejecute antes alguna celda.", + "No/A table cell answering whether a setting is read-only. The answer to a question, not the word for a number.": "No", + "Notebook Files/Names the kind of file the box will accept.": "Archivos de cuaderno", + "Notebook input/Asked when a running cell wants something typed and did not say what.": "Entrada del cuaderno", + "Property/A table heading: the name of one setting on a cell.": "Propiedad", + "Provider: {0}/Names the extension a group of settings came from. {0} is its id.": "Proveedor: {0}", + "Read-only/A table heading: whether a setting can be changed.": "Solo lectura", + "Remove cell": "Quitar la celda", + "Remove cell **{0}** from the notebook?": "¿Quitar la celda **{0}** del cuaderno?", + "Remove parameter": "Quitar el parámetro", + "Remove parameter **{0}** from the notebook?": "¿Quitar el parámetro **{0}** del cuaderno?", + "Removing cell {0}": "Quitando la celda {0}", + "Removing parameter \"{0}\"": "Quitando el parámetro \"{0}\"", + "Restart aborted: the notebook snapshot could not be captured ({0}).": "Reinicio cancelado: no se pudo capturar la instantánea del cuaderno ({0}).", + "Running all cells": "Ejecutando todas las celdas", + "Running all cells...": "Ejecutando todas las celdas...", + "Running cell {0}": "Ejecutando la celda {0}", + "Select a notebook for @verso/@verso is typed to address the assistant and stays as written.": "Seleccione un cuaderno para @verso", + "Setting \"{0}\" on cell {1}": "Estableciendo \"{0}\" en la celda {1}", + "Setup Help/A button. It opens the page describing how to set Verso up.": "Ayuda de configuración", + "Switching layout to \"{0}\"": "Cambiando el diseño a \"{0}\"", + "The notebook file is not inside a git repository.": "El archivo del cuaderno no está dentro de un repositorio git.", + "The notebook has no file on disk yet.": "El cuaderno todavía no tiene ningún archivo en el disco.", + "The notebook is empty.": "El cuaderno está vacío.", + "This comparison source is not available.": "Este origen de comparación no está disponible.", + "This notebook is not inside a git repository.": "Este cuaderno no está dentro de un repositorio git.", + "Type a ref or commit.../The last entry in a list of branches and tags, for naming one that is not listed. Ref and commit are version control terms.": "Escriba una Ref o una confirmación...", + "Type/A table heading: what kind of value a setting takes.": "Tipo", + "Type/A table heading: what kind of value a variable holds.": "Tipo", + "Unknown comparison source '{0}'.": "Origen de comparación '{0}' desconocido.", + "Updating cell {0}": "Actualizando la celda {0}", + "Updating parameter \"{0}\"": "Actualizando el parámetro \"{0}\"", + "Usage: `/props ` (1-based). Example: `/props 2`/Shown when /props was typed without a cell number. Everything in backticks is typed and stays as written.": "Uso: `/props ` (empezando por 1). Ejemplo: `/props 2`", + "Value/A table heading: what a setting is currently set to.": "Valor", + "Value/A table heading: what a variable currently holds.": "Valor", + "Verso host process exited ({0})": "El proceso host de Verso terminó ({0})", + "Verso needs the .NET runtime (version {0} or later) to run notebooks, but a compatible installation was not found.": "Verso necesita el runtime de .NET (versión {0} o posterior) para ejecutar cuadernos, pero no se encontró ninguna instalación compatible.", + "Verso: Could not create a scratch notebook: {0}": "Verso: No se pudo crear un cuaderno de borrador: {0}", + "Verso: Could not find Verso.Host.dll. Set \"verso.hostPath\" in settings to the path of your built Verso.Host.dll.": "Verso: No se encontró Verso.Host.dll. Establezca \"verso.hostPath\" en la configuración con la ruta del Verso.Host.dll compilado.", + "Verso: Failed to open notebook: {0}": "Verso: No se pudo abrir el cuaderno: {0}", + "Verso: Failed to start host process: {0}": "Verso: No se pudo iniciar el proceso host: {0}", + "Verso: Open a notebook to compare it with a baseline.": "Verso: Abra un cuaderno para compararlo con una línea base.", + "Verso: installing the .NET runtime...": "Verso: instalando el runtime de .NET...", + "Verso: kernel restart aborted because the notebook snapshot could not be captured. Save and reopen the file.": "Verso: se canceló el reinicio del kernel porque no se pudo capturar la instantánea del cuaderno. Guarde y vuelva a abrir el archivo.", + "Verso: kernel restart failed (notebook did not reopen): {0}. Close and reopen the notebook.": "Verso: el reinicio del kernel falló (el cuaderno no se volvió a abrir): {0}. Cierre y vuelva a abrir el cuaderno.", + "Verso: the .NET runtime is installed. Reopen the notebook to continue.": "Verso: el runtime de .NET está instalado. Vuelva a abrir el cuaderno para continuar.", + "Verso: the notebooks already open keep their current language. Reopen them to read them in the new one.": "Verso: los cuadernos que ya están abiertos conservan su idioma actual. Vuelva a abrirlos para leerlos en el nuevo.", + "Yes/A table cell answering whether a setting is read-only.": "Sí", + "branch/Says what kind of thing is listed, shown beside its name. A line of work in version control.": "rama", + "git could not read '{0}' at '{1}': {2}": "git no pudo leer '{0}' en '{1}': {2}", + "remote branch/A branch that lives on the server rather than on this machine.": "rama remota", + "tag/A name pinned to one point in a project's history.": "etiqueta", + "unavailable/Said of a baseline that cannot be compared against, for a reason nothing here knows.": "no disponible", + "{0} Files/Names the kind of file a save box will accept. {0} is a format name such as CSV or HTML and is the same word in every language.": "Archivos {0}", + "{0} cell/Used when {0} is 1. Paired with the entry below.": "{0} celda", + "{0} cells/Used for every count other than 1. A language with one form for both translates this the same as the entry above.": "{0} celdas", + "{0} line/Used when {0} is 1. Paired with the entry below.": "{0} línea", + "{0} lines/Used for every count other than 1. A language with one form for both translates this the same as the entry above.": "{0} líneas" +} diff --git a/vscode/l10n/bundle.l10n.ja.json b/vscode/l10n/bundle.l10n.ja.json new file mode 100644 index 00000000..5d3aa6fa --- /dev/null +++ b/vscode/l10n/bundle.l10n.ja.json @@ -0,0 +1,96 @@ +{ + "'{0}' is not a known branch, tag, or commit.": "'{0}' は既知のブランチ、タグ、コミットのいずれでもありません。", + "'{0}' is not tracked at '{1}'. Commit the file first, or pick a different ref.": "'{0}' は '{1}' で追跡されていません。先にコミットするか、別の参照を選んでください。", + "(not set)/Stands in a table cell for a setting that has no value yet.": "(未設定)", + "(unnamed)/Stands in for a branch or tag that has no name to show.": "(名前なし)", + "**Cell {0}** [{1}] properties:": "**セル {0}** [{1}] のプロパティ:", + "Adding parameter \"{0}\" ({1})": "パラメーター \"{0}\" ({1}) を追加しています", + "Adding {0} cell ({1})": "{0} セル ({1}) を追加しています", + "All Files/The entry in a save box that accepts any file at all.": "すべてのファイル", + "Cell ({0}ms)/{0} is a number of milliseconds; ms is the unit and stays as written.": "セル ({0}ms)", + "Cell {0} has no configurable properties.": "セル {0} に設定できるプロパティはありません。", + "Cell {0} not found. The notebook has {1}.": "セル {0} が見つかりません。このノートブックには {1} があります。", + "Cell {0}/A heading over one cell's code. {0} counts from 1.": "セル {0}", + "Changing cell {0} language to \"{1}\"": "セル {0} の言語を \"{1}\" に変更しています", + "Changing cell {0} to type \"{1}\"": "セル {0} のタイプを \"{1}\" に変更しています", + "Choose File.../Opens a box for picking another notebook to compare against. Keep the three dots, which mean a question follows.": "ファイルを選択...", + "Compare notebook with...": "ノートブックの比較対象...", + "Compare/The button that accepts the chosen file, in place of \"Open\".": "比較", + "Error: {0}/{0} is what the cell reported, in the language the kernel reported it.": "エラー: {0}", + "Export failed: {0}": "エクスポートに失敗しました: {0}", + "Exported to {0}": "{0} にエクスポートしました", + "Extensions/Names the kind of file the box will accept.": "拡張機能", + "Git branch, tag, or commit SHA/Says what may be typed. Every term here is a version control term and stays as written.": "Git のブランチ、タグ、またはコミット SHA", + "Git: Compare with Ref.../Opens a box for choosing a branch, tag, or commit. Git and Ref are version control terms and are not translated. Keep the three dots, which mean a question follows.": "Git: Ref と比較...", + "Git: HEAD/One of the things a notebook can be compared against. Git and HEAD are version control terms and are not translated.": "Git: HEAD", + "Images/Names the kind of file a save box will accept: pictures.": "画像", + "Input request failed: {0}": "入力の要求に失敗しました: {0}", + "Install .NET Runtime/A button. .NET is a product name and stays as written.": ".NET ランタイムをインストール", + "Install Extension/The button that accepts the chosen file, in place of \"Open\".": "拡張機能をインストール", + "Kernel restart failed: the host process did not start ({0}). Close and reopen the notebook.": "カーネルの再起動に失敗しました: ホストプロセスが起動しませんでした ({0})。ノートブックを閉じて開き直してください。", + "Kernel restart failed: the notebook did not reopen ({0}).": "カーネルの再起動に失敗しました: ノートブックが開き直されませんでした ({0})。", + "Last Saved/One of the things a notebook can be compared against: the copy currently on disk.": "最後に保存した状態", + "Model error: {0}": "モデルのエラー: {0}", + "Moving cell {0} to position {1}": "セル {0} を位置 {1} に移動しています", + "Name/A table heading: what a variable is called.": "名前", + "No Verso notebook is currently open.": "Verso ノートブックが開かれていません。", + "No Verso notebook is currently open. Open a `.verso`, `.ipynb`, `.md`, or `.dib` file first.": "Verso ノートブックが開かれていません。先に `.verso`、`.ipynb`、`.md`、`.dib` のいずれかのファイルを開いてください。", + "No variables in scope. Run some cells first.": "スコープ内に変数がありません。先にセルを実行してください。", + "No/A table cell answering whether a setting is read-only. The answer to a question, not the word for a number.": "いいえ", + "Notebook Files/Names the kind of file the box will accept.": "ノートブックファイル", + "Notebook input/Asked when a running cell wants something typed and did not say what.": "ノートブックの入力", + "Property/A table heading: the name of one setting on a cell.": "プロパティ", + "Provider: {0}/Names the extension a group of settings came from. {0} is its id.": "提供元: {0}", + "Read-only/A table heading: whether a setting can be changed.": "読み取り専用", + "Remove cell": "セルを削除", + "Remove cell **{0}** from the notebook?": "セル **{0}** をノートブックから削除しますか?", + "Remove parameter": "パラメーターを削除", + "Remove parameter **{0}** from the notebook?": "パラメーター **{0}** をノートブックから削除しますか?", + "Removing cell {0}": "セル {0} を削除しています", + "Removing parameter \"{0}\"": "パラメーター \"{0}\" を削除しています", + "Restart aborted: the notebook snapshot could not be captured ({0}).": "再起動を中止しました: ノートブックのスナップショットを取得できませんでした ({0})。", + "Running all cells": "すべてのセルを実行しています", + "Running all cells...": "すべてのセルを実行しています...", + "Running cell {0}": "セル {0} を実行しています", + "Select a notebook for @verso/@verso is typed to address the assistant and stays as written.": "@verso で使うノートブックを選択", + "Setting \"{0}\" on cell {1}": "セル {1} に \"{0}\" を設定しています", + "Setup Help/A button. It opens the page describing how to set Verso up.": "セットアップのヘルプ", + "Switching layout to \"{0}\"": "レイアウトを \"{0}\" に切り替えています", + "The notebook file is not inside a git repository.": "ノートブックファイルは git リポジトリの中にありません。", + "The notebook has no file on disk yet.": "ノートブックはまだディスク上のファイルになっていません。", + "The notebook is empty.": "ノートブックが空です。", + "This comparison source is not available.": "この比較対象は利用できません。", + "This notebook is not inside a git repository.": "このノートブックは git リポジトリの中にありません。", + "Type a ref or commit.../The last entry in a list of branches and tags, for naming one that is not listed. Ref and commit are version control terms.": "ref またはコミットを入力...", + "Type/A table heading: what kind of value a setting takes.": "型", + "Type/A table heading: what kind of value a variable holds.": "型", + "Unknown comparison source '{0}'.": "不明な比較対象 '{0}' です。", + "Updating cell {0}": "セル {0} を更新しています", + "Updating parameter \"{0}\"": "パラメーター \"{0}\" を更新しています", + "Usage: `/props ` (1-based). Example: `/props 2`/Shown when /props was typed without a cell number. Everything in backticks is typed and stays as written.": "使い方: `/props ` (1 から数えます)。例: `/props 2`", + "Value/A table heading: what a setting is currently set to.": "値", + "Value/A table heading: what a variable currently holds.": "値", + "Verso host process exited ({0})": "Verso のホストプロセスが終了しました ({0})", + "Verso needs the .NET runtime (version {0} or later) to run notebooks, but a compatible installation was not found.": "Verso がノートブックを実行するには .NET ランタイム (バージョン {0} 以降) が必要ですが、対応するインストールが見つかりませんでした。", + "Verso: Could not create a scratch notebook: {0}": "Verso: スクラッチノートブックを作成できませんでした: {0}", + "Verso: Could not find Verso.Host.dll. Set \"verso.hostPath\" in settings to the path of your built Verso.Host.dll.": "Verso: Verso.Host.dll が見つかりません。設定の \"verso.hostPath\" に、ビルドした Verso.Host.dll のパスを指定してください。", + "Verso: Failed to open notebook: {0}": "Verso: ノートブックを開けませんでした: {0}", + "Verso: Failed to start host process: {0}": "Verso: ホストプロセスを開始できませんでした: {0}", + "Verso: Open a notebook to compare it with a baseline.": "Verso: ベースラインと比較するには、ノートブックを開いてください。", + "Verso: installing the .NET runtime...": "Verso: .NET ランタイムをインストールしています...", + "Verso: kernel restart aborted because the notebook snapshot could not be captured. Save and reopen the file.": "Verso: ノートブックのスナップショットを取得できなかったため、カーネルの再起動を中止しました。ファイルを保存して開き直してください。", + "Verso: kernel restart failed (notebook did not reopen): {0}. Close and reopen the notebook.": "Verso: カーネルの再起動に失敗しました (ノートブックが開き直されませんでした): {0}。ノートブックを閉じて開き直してください。", + "Verso: the .NET runtime is installed. Reopen the notebook to continue.": "Verso: .NET ランタイムをインストールしました。続けるにはノートブックを開き直してください。", + "Verso: the notebooks already open keep their current language. Reopen them to read them in the new one.": "Verso: 既に開いているノートブックは現在の言語のままです。新しい言語で読むには開き直してください。", + "Yes/A table cell answering whether a setting is read-only.": "はい", + "branch/Says what kind of thing is listed, shown beside its name. A line of work in version control.": "ブランチ", + "git could not read '{0}' at '{1}': {2}": "git は '{1}' の '{0}' を読み取れませんでした: {2}", + "remote branch/A branch that lives on the server rather than on this machine.": "リモートブランチ", + "tag/A name pinned to one point in a project's history.": "タグ", + "unavailable/Said of a baseline that cannot be compared against, for a reason nothing here knows.": "利用できません", + "{0} Files/Names the kind of file a save box will accept. {0} is a format name such as CSV or HTML and is the same word in every language.": "{0} ファイル", + "{0} cell/Used when {0} is 1. Paired with the entry below.": "{0} 個のセル", + "{0} cells/Used for every count other than 1. A language with one form for both translates this the same as the entry above.": "{0} 個のセル", + "{0} line/Used when {0} is 1. Paired with the entry below.": "{0} 行", + "{0} lines/Used for every count other than 1. A language with one form for both translates this the same as the entry above.": "{0} 行" +} diff --git a/vscode/l10n/bundle.l10n.json b/vscode/l10n/bundle.l10n.json new file mode 100644 index 00000000..39e86fef --- /dev/null +++ b/vscode/l10n/bundle.l10n.json @@ -0,0 +1,301 @@ +{ + "Verso: Could not find Verso.Host.dll. Set \"verso.hostPath\" in settings to the path of your built Verso.Host.dll.": "Verso: Could not find Verso.Host.dll. Set \"verso.hostPath\" in settings to the path of your built Verso.Host.dll.", + "Verso: Open a notebook to compare it with a baseline.": "Verso: Open a notebook to compare it with a baseline.", + "unavailable/Said of a baseline that cannot be compared against, for a reason nothing here knows.": { + "message": "unavailable", + "comment": [ + "Said of a baseline that cannot be compared against, for a reason nothing here knows." + ] + }, + "Compare notebook with...": "Compare notebook with...", + "This comparison source is not available.": "This comparison source is not available.", + "Verso host process exited ({0})": "Verso host process exited ({0})", + "Verso: Failed to start host process: {0}": "Verso: Failed to start host process: {0}", + "Verso needs the .NET runtime (version {0} or later) to run notebooks, but a compatible installation was not found.": "Verso needs the .NET runtime (version {0} or later) to run notebooks, but a compatible installation was not found.", + "Install .NET Runtime/A button. .NET is a product name and stays as written.": { + "message": "Install .NET Runtime", + "comment": [ + "A button. .NET is a product name and stays as written." + ] + }, + "Setup Help/A button. It opens the page describing how to set Verso up.": { + "message": "Setup Help", + "comment": [ + "A button. It opens the page describing how to set Verso up." + ] + }, + "Verso: the .NET runtime is installed. Reopen the notebook to continue.": "Verso: the .NET runtime is installed. Reopen the notebook to continue.", + "Verso: installing the .NET runtime...": "Verso: installing the .NET runtime...", + "This notebook is not inside a git repository.": "This notebook is not inside a git repository.", + "(unnamed)/Stands in for a branch or tag that has no name to show.": { + "message": "(unnamed)", + "comment": [ + "Stands in for a branch or tag that has no name to show." + ] + }, + "branch/Says what kind of thing is listed, shown beside its name. A line of work in version control.": { + "message": "branch", + "comment": [ + "Says what kind of thing is listed, shown beside its name. A line of work in version control." + ] + }, + "remote branch/A branch that lives on the server rather than on this machine.": { + "message": "remote branch", + "comment": [ + "A branch that lives on the server rather than on this machine." + ] + }, + "tag/A name pinned to one point in a project's history.": { + "message": "tag", + "comment": [ + "A name pinned to one point in a project's history." + ] + }, + "'{0}' is not tracked at '{1}'. Commit the file first, or pick a different ref.": "'{0}' is not tracked at '{1}'. Commit the file first, or pick a different ref.", + "'{0}' is not a known branch, tag, or commit.": "'{0}' is not a known branch, tag, or commit.", + "git could not read '{0}' at '{1}': {2}": "git could not read '{0}' at '{1}': {2}", + "Select a notebook for @verso/@verso is typed to address the assistant and stays as written.": { + "message": "Select a notebook for @verso", + "comment": [ + "@verso is typed to address the assistant and stays as written." + ] + }, + "{0} line/Used when {0} is 1. Paired with the entry below.": { + "message": "{0} line", + "comment": [ + "Used when {0} is 1. Paired with the entry below." + ] + }, + "{0} lines/Used for every count other than 1. A language with one form for both translates this the same as the entry above.": { + "message": "{0} lines", + "comment": [ + "Used for every count other than 1. A language with one form for both translates this the same as the entry above." + ] + }, + "Adding {0} cell ({1})": "Adding {0} cell ({1})", + "Updating cell {0}": "Updating cell {0}", + "Removing cell {0}": "Removing cell {0}", + "Remove cell": "Remove cell", + "Remove cell **{0}** from the notebook?": "Remove cell **{0}** from the notebook?", + "Running cell {0}": "Running cell {0}", + "Running all cells": "Running all cells", + "Adding parameter \"{0}\" ({1})": "Adding parameter \"{0}\" ({1})", + "Updating parameter \"{0}\"": "Updating parameter \"{0}\"", + "Removing parameter \"{0}\"": "Removing parameter \"{0}\"", + "Remove parameter": "Remove parameter", + "Remove parameter **{0}** from the notebook?": "Remove parameter **{0}** from the notebook?", + "Setting \"{0}\" on cell {1}": "Setting \"{0}\" on cell {1}", + "Switching layout to \"{0}\"": "Switching layout to \"{0}\"", + "Moving cell {0} to position {1}": "Moving cell {0} to position {1}", + "Changing cell {0} to type \"{1}\"": "Changing cell {0} to type \"{1}\"", + "Changing cell {0} language to \"{1}\"": "Changing cell {0} language to \"{1}\"", + "No Verso notebook is currently open.": "No Verso notebook is currently open.", + "{0} cell/Used when {0} is 1. Paired with the entry below.": { + "message": "{0} cell", + "comment": [ + "Used when {0} is 1. Paired with the entry below." + ] + }, + "{0} cells/Used for every count other than 1. A language with one form for both translates this the same as the entry above.": { + "message": "{0} cells", + "comment": [ + "Used for every count other than 1. A language with one form for both translates this the same as the entry above." + ] + }, + "Cell {0} not found. The notebook has {1}.": "Cell {0} not found. The notebook has {1}.", + "The notebook is empty.": "The notebook is empty.", + "Cell {0}/A heading over one cell's code. {0} counts from 1.": { + "message": "Cell {0}", + "comment": [ + "A heading over one cell's code. {0} counts from 1." + ] + }, + "Running all cells...": "Running all cells...", + "Cell ({0}ms)/{0} is a number of milliseconds; ms is the unit and stays as written.": { + "message": "Cell ({0}ms)", + "comment": [ + "{0} is a number of milliseconds; ms is the unit and stays as written." + ] + }, + "Error: {0}/{0} is what the cell reported, in the language the kernel reported it.": { + "message": "Error: {0}", + "comment": [ + "{0} is what the cell reported, in the language the kernel reported it." + ] + }, + "No variables in scope. Run some cells first.": "No variables in scope. Run some cells first.", + "Name/A table heading: what a variable is called.": { + "message": "Name", + "comment": [ + "A table heading: what a variable is called." + ] + }, + "Type/A table heading: what kind of value a variable holds.": { + "message": "Type", + "comment": [ + "A table heading: what kind of value a variable holds." + ] + }, + "Value/A table heading: what a variable currently holds.": { + "message": "Value", + "comment": [ + "A table heading: what a variable currently holds." + ] + }, + "Usage: `/props ` (1-based). Example: `/props 2`/Shown when /props was typed without a cell number. Everything in backticks is typed and stays as written.": { + "message": "Usage: `/props ` (1-based). Example: `/props 2`", + "comment": [ + "Shown when /props was typed without a cell number. Everything in backticks is typed and stays as written." + ] + }, + "Cell {0} has no configurable properties.": "Cell {0} has no configurable properties.", + "**Cell {0}** [{1}] properties:": "**Cell {0}** [{1}] properties:", + "Property/A table heading: the name of one setting on a cell.": { + "message": "Property", + "comment": [ + "A table heading: the name of one setting on a cell." + ] + }, + "Type/A table heading: what kind of value a setting takes.": { + "message": "Type", + "comment": [ + "A table heading: what kind of value a setting takes." + ] + }, + "Value/A table heading: what a setting is currently set to.": { + "message": "Value", + "comment": [ + "A table heading: what a setting is currently set to." + ] + }, + "Read-only/A table heading: whether a setting can be changed.": { + "message": "Read-only", + "comment": [ + "A table heading: whether a setting can be changed." + ] + }, + "(not set)/Stands in a table cell for a setting that has no value yet.": { + "message": "(not set)", + "comment": [ + "Stands in a table cell for a setting that has no value yet." + ] + }, + "Yes/A table cell answering whether a setting is read-only.": { + "message": "Yes", + "comment": [ + "A table cell answering whether a setting is read-only." + ] + }, + "No/A table cell answering whether a setting is read-only. The answer to a question, not the word for a number.": { + "message": "No", + "comment": [ + "A table cell answering whether a setting is read-only. The answer to a question, not the word for a number." + ] + }, + "Provider: {0}/Names the extension a group of settings came from. {0} is its id.": { + "message": "Provider: {0}", + "comment": [ + "Names the extension a group of settings came from. {0} is its id." + ] + }, + "No Verso notebook is currently open. Open a `.verso`, `.ipynb`, `.md`, or `.dib` file first.": "No Verso notebook is currently open. Open a `.verso`, `.ipynb`, `.md`, or `.dib` file first.", + "Model error: {0}": "Model error: {0}", + "Verso: the notebooks already open keep their current language. Reopen them to read them in the new one.": "Verso: the notebooks already open keep their current language. Reopen them to read them in the new one.", + "Verso: Could not create a scratch notebook: {0}": "Verso: Could not create a scratch notebook: {0}", + "Verso: Failed to open notebook: {0}": "Verso: Failed to open notebook: {0}", + "Restart aborted: the notebook snapshot could not be captured ({0}).": "Restart aborted: the notebook snapshot could not be captured ({0}).", + "Verso: kernel restart aborted because the notebook snapshot could not be captured. Save and reopen the file.": "Verso: kernel restart aborted because the notebook snapshot could not be captured. Save and reopen the file.", + "Kernel restart failed: the host process did not start ({0}). Close and reopen the notebook.": "Kernel restart failed: the host process did not start ({0}). Close and reopen the notebook.", + "Kernel restart failed: the notebook did not reopen ({0}).": "Kernel restart failed: the notebook did not reopen ({0}).", + "Verso: kernel restart failed (notebook did not reopen): {0}. Close and reopen the notebook.": "Verso: kernel restart failed (notebook did not reopen): {0}. Close and reopen the notebook.", + "Export failed: {0}": "Export failed: {0}", + "Input request failed: {0}": "Input request failed: {0}", + "Install Extension/The button that accepts the chosen file, in place of \"Open\".": { + "message": "Install Extension", + "comment": [ + "The button that accepts the chosen file, in place of \"Open\"." + ] + }, + "Extensions/Names the kind of file the box will accept.": { + "message": "Extensions", + "comment": [ + "Names the kind of file the box will accept." + ] + }, + "Exported to {0}": "Exported to {0}", + "Notebook input/Asked when a running cell wants something typed and did not say what.": { + "message": "Notebook input", + "comment": [ + "Asked when a running cell wants something typed and did not say what." + ] + }, + "{0} Files/Names the kind of file a save box will accept. {0} is a format name such as CSV or HTML and is the same word in every language.": { + "message": "{0} Files", + "comment": [ + "Names the kind of file a save box will accept. {0} is a format name such as CSV or HTML and is the same word in every language." + ] + }, + "Images/Names the kind of file a save box will accept: pictures.": { + "message": "Images", + "comment": [ + "Names the kind of file a save box will accept: pictures." + ] + }, + "All Files/The entry in a save box that accepts any file at all.": { + "message": "All Files", + "comment": [ + "The entry in a save box that accepts any file at all." + ] + }, + "The notebook file is not inside a git repository.": "The notebook file is not inside a git repository.", + "Last Saved/One of the things a notebook can be compared against: the copy currently on disk.": { + "message": "Last Saved", + "comment": [ + "One of the things a notebook can be compared against: the copy currently on disk." + ] + }, + "The notebook has no file on disk yet.": "The notebook has no file on disk yet.", + "Git: HEAD/One of the things a notebook can be compared against. Git and HEAD are version control terms and are not translated.": { + "message": "Git: HEAD", + "comment": [ + "One of the things a notebook can be compared against. Git and HEAD are version control terms and are not translated." + ] + }, + "Git: Compare with Ref.../Opens a box for choosing a branch, tag, or commit. Git and Ref are version control terms and are not translated. Keep the three dots, which mean a question follows.": { + "message": "Git: Compare with Ref...", + "comment": [ + "Opens a box for choosing a branch, tag, or commit. Git and Ref are version control terms and are not translated. Keep the three dots, which mean a question follows." + ] + }, + "Choose File.../Opens a box for picking another notebook to compare against. Keep the three dots, which mean a question follows.": { + "message": "Choose File...", + "comment": [ + "Opens a box for picking another notebook to compare against. Keep the three dots, which mean a question follows." + ] + }, + "Type a ref or commit.../The last entry in a list of branches and tags, for naming one that is not listed. Ref and commit are version control terms.": { + "message": "Type a ref or commit...", + "comment": [ + "The last entry in a list of branches and tags, for naming one that is not listed. Ref and commit are version control terms." + ] + }, + "Git branch, tag, or commit SHA/Says what may be typed. Every term here is a version control term and stays as written.": { + "message": "Git branch, tag, or commit SHA", + "comment": [ + "Says what may be typed. Every term here is a version control term and stays as written." + ] + }, + "Compare/The button that accepts the chosen file, in place of \"Open\".": { + "message": "Compare", + "comment": [ + "The button that accepts the chosen file, in place of \"Open\"." + ] + }, + "Notebook Files/Names the kind of file the box will accept.": { + "message": "Notebook Files", + "comment": [ + "Names the kind of file the box will accept." + ] + }, + "Unknown comparison source '{0}'.": "Unknown comparison source '{0}'." +} \ No newline at end of file diff --git a/vscode/l10n/bundle.l10n.qps-ploc.json b/vscode/l10n/bundle.l10n.qps-ploc.json new file mode 100644 index 00000000..86e7bd90 --- /dev/null +++ b/vscode/l10n/bundle.l10n.qps-ploc.json @@ -0,0 +1,96 @@ +{ + "'{0}' is not a known branch, tag, or commit.": "[!!'{0}' ïš ñòt à kñòwñ bràñçh, tàg, òr çòmmït.···!!]", + "'{0}' is not tracked at '{1}'. Commit the file first, or pick a different ref.": "[!!'{0}' ïš ñòt tràçkéd àt '{1}'. Çòmmït thé fïlé fïršt, òr pïçk à dïfféréñt réf.···!!]", + "(not set)/Stands in a table cell for a setting that has no value yet.": "[!!(ñòt šét)···!!]", + "(unnamed)/Stands in for a branch or tag that has no name to show.": "[!!(ùññàméd)···!!]", + "**Cell {0}** [{1}] properties:": "[!!**Çéll {0}** [{1}] pròpértïéš:···!!]", + "Adding parameter \"{0}\" ({1})": "[!!Àddïñg pàràmétér \"{0}\" ({1})···!!]", + "Adding {0} cell ({1})": "[!!Àddïñg {0} çéll ({1})···!!]", + "All Files/The entry in a save box that accepts any file at all.": "[!!Àll Fïléš···!!]", + "Cell ({0}ms)/{0} is a number of milliseconds; ms is the unit and stays as written.": "[!!Çéll ({0}mš)···!!]", + "Cell {0} has no configurable properties.": "[!!Çéll {0} hàš ñò çòñfïgùràblé pròpértïéš.···!!]", + "Cell {0} not found. The notebook has {1}.": "[!!Çéll {0} ñòt fòùñd. Thé ñòtébòòk hàš {1}.···!!]", + "Cell {0}/A heading over one cell's code. {0} counts from 1.": "[!!Çéll {0}···!!]", + "Changing cell {0} language to \"{1}\"": "[!!Çhàñgïñg çéll {0} làñgùàgé tò \"{1}\"···!!]", + "Changing cell {0} to type \"{1}\"": "[!!Çhàñgïñg çéll {0} tò tÿpé \"{1}\"···!!]", + "Choose File.../Opens a box for picking another notebook to compare against. Keep the three dots, which mean a question follows.": "[!!Çhòòšé Fïlé...···!!]", + "Compare notebook with...": "[!!Çòmpàré ñòtébòòk wïth...···!!]", + "Compare/The button that accepts the chosen file, in place of \"Open\".": "[!!Çòmpàré···!!]", + "Error: {0}/{0} is what the cell reported, in the language the kernel reported it.": "[!!Érròr: {0}···!!]", + "Export failed: {0}": "[!!Éxpòrt fàïléd: {0}···!!]", + "Exported to {0}": "[!!Éxpòrtéd tò {0}···!!]", + "Extensions/Names the kind of file the box will accept.": "[!!Éxtéñšïòñš···!!]", + "Git branch, tag, or commit SHA/Says what may be typed. Every term here is a version control term and stays as written.": "[!!Gït bràñçh, tàg, òr çòmmït ŠHÀ···!!]", + "Git: Compare with Ref.../Opens a box for choosing a branch, tag, or commit. Git and Ref are version control terms and are not translated. Keep the three dots, which mean a question follows.": "[!!Gït: Çòmpàré wïth Réf...···!!]", + "Git: HEAD/One of the things a notebook can be compared against. Git and HEAD are version control terms and are not translated.": "[!!Gït: HÉÀD···!!]", + "Images/Names the kind of file a save box will accept: pictures.": "[!!Ïmàgéš···!!]", + "Input request failed: {0}": "[!!Ïñpùt réqùéšt fàïléd: {0}···!!]", + "Install .NET Runtime/A button. .NET is a product name and stays as written.": "[!!Ïñštàll .ÑÉT Rùñtïmé···!!]", + "Install Extension/The button that accepts the chosen file, in place of \"Open\".": "[!!Ïñštàll Éxtéñšïòñ···!!]", + "Kernel restart failed: the host process did not start ({0}). Close and reopen the notebook.": "[!!Kérñél réštàrt fàïléd: thé hòšt pròçéšš dïd ñòt štàrt ({0}). Çlòšé àñd réòpéñ thé ñòtébòòk.···!!]", + "Kernel restart failed: the notebook did not reopen ({0}).": "[!!Kérñél réštàrt fàïléd: thé ñòtébòòk dïd ñòt réòpéñ ({0}).···!!]", + "Last Saved/One of the things a notebook can be compared against: the copy currently on disk.": "[!!Làšt Šàvéd···!!]", + "Model error: {0}": "[!!Mòdél érròr: {0}···!!]", + "Moving cell {0} to position {1}": "[!!Mòvïñg çéll {0} tò pòšïtïòñ {1}···!!]", + "Name/A table heading: what a variable is called.": "[!!Ñàmé···!!]", + "No Verso notebook is currently open.": "[!!Ñò Véršò ñòtébòòk ïš çùrréñtlÿ òpéñ.···!!]", + "No Verso notebook is currently open. Open a `.verso`, `.ipynb`, `.md`, or `.dib` file first.": "[!!Ñò Véršò ñòtébòòk ïš çùrréñtlÿ òpéñ. Òpéñ à `.véršò`, `.ïpÿñb`, `.md`, òr `.dïb` fïlé fïršt.···!!]", + "No variables in scope. Run some cells first.": "[!!Ñò vàrïàbléš ïñ šçòpé. Rùñ šòmé çéllš fïršt.···!!]", + "No/A table cell answering whether a setting is read-only. The answer to a question, not the word for a number.": "[!!Ñò···!!]", + "Notebook Files/Names the kind of file the box will accept.": "[!!Ñòtébòòk Fïléš···!!]", + "Notebook input/Asked when a running cell wants something typed and did not say what.": "[!!Ñòtébòòk ïñpùt···!!]", + "Property/A table heading: the name of one setting on a cell.": "[!!Pròpértÿ···!!]", + "Provider: {0}/Names the extension a group of settings came from. {0} is its id.": "[!!Pròvïdér: {0}···!!]", + "Read-only/A table heading: whether a setting can be changed.": "[!!Réàd-òñlÿ···!!]", + "Remove cell": "[!!Rémòvé çéll···!!]", + "Remove cell **{0}** from the notebook?": "[!!Rémòvé çéll **{0}** fròm thé ñòtébòòk?···!!]", + "Remove parameter": "[!!Rémòvé pàràmétér···!!]", + "Remove parameter **{0}** from the notebook?": "[!!Rémòvé pàràmétér **{0}** fròm thé ñòtébòòk?···!!]", + "Removing cell {0}": "[!!Rémòvïñg çéll {0}···!!]", + "Removing parameter \"{0}\"": "[!!Rémòvïñg pàràmétér \"{0}\"···!!]", + "Restart aborted: the notebook snapshot could not be captured ({0}).": "[!!Réštàrt àbòrtéd: thé ñòtébòòk šñàpšhòt çòùld ñòt bé çàptùréd ({0}).···!!]", + "Running all cells": "[!!Rùññïñg àll çéllš···!!]", + "Running all cells...": "[!!Rùññïñg àll çéllš...···!!]", + "Running cell {0}": "[!!Rùññïñg çéll {0}···!!]", + "Select a notebook for @verso/@verso is typed to address the assistant and stays as written.": "[!!Šéléçt à ñòtébòòk fòr @véršò···!!]", + "Setting \"{0}\" on cell {1}": "[!!Šéttïñg \"{0}\" òñ çéll {1}···!!]", + "Setup Help/A button. It opens the page describing how to set Verso up.": "[!!Šétùp Hélp···!!]", + "Switching layout to \"{0}\"": "[!!Šwïtçhïñg làÿòùt tò \"{0}\"···!!]", + "The notebook file is not inside a git repository.": "[!!Thé ñòtébòòk fïlé ïš ñòt ïñšïdé à gït répòšïtòrÿ.···!!]", + "The notebook has no file on disk yet.": "[!!Thé ñòtébòòk hàš ñò fïlé òñ dïšk ÿét.···!!]", + "The notebook is empty.": "[!!Thé ñòtébòòk ïš émptÿ.···!!]", + "This comparison source is not available.": "[!!Thïš çòmpàrïšòñ šòùrçé ïš ñòt àvàïlàblé.···!!]", + "This notebook is not inside a git repository.": "[!!Thïš ñòtébòòk ïš ñòt ïñšïdé à gït répòšïtòrÿ.···!!]", + "Type a ref or commit.../The last entry in a list of branches and tags, for naming one that is not listed. Ref and commit are version control terms.": "[!!Tÿpé à réf òr çòmmït...···!!]", + "Type/A table heading: what kind of value a setting takes.": "[!!Tÿpé···!!]", + "Type/A table heading: what kind of value a variable holds.": "[!!Tÿpé···!!]", + "Unknown comparison source '{0}'.": "[!!Ùñkñòwñ çòmpàrïšòñ šòùrçé '{0}'.···!!]", + "Updating cell {0}": "[!!Ùpdàtïñg çéll {0}···!!]", + "Updating parameter \"{0}\"": "[!!Ùpdàtïñg pàràmétér \"{0}\"···!!]", + "Usage: `/props ` (1-based). Example: `/props 2`/Shown when /props was typed without a cell number. Everything in backticks is typed and stays as written.": "[!!Ùšàgé: `/pròpš <çéll ñùmbér>` (1-bàšéd). Éxàmplé: `/pròpš 2`···!!]", + "Value/A table heading: what a setting is currently set to.": "[!!Vàlùé···!!]", + "Value/A table heading: what a variable currently holds.": "[!!Vàlùé···!!]", + "Verso host process exited ({0})": "[!!Véršò hòšt pròçéšš éxïtéd ({0})···!!]", + "Verso needs the .NET runtime (version {0} or later) to run notebooks, but a compatible installation was not found.": "[!!Véršò ñéédš thé .ÑÉT rùñtïmé (véršïòñ {0} òr làtér) tò rùñ ñòtébòòkš, bùt à çòmpàtïblé ïñštàllàtïòñ wàš ñòt fòùñd.···!!]", + "Verso: Could not create a scratch notebook: {0}": "[!!Véršò: Çòùld ñòt çréàté à šçràtçh ñòtébòòk: {0}···!!]", + "Verso: Could not find Verso.Host.dll. Set \"verso.hostPath\" in settings to the path of your built Verso.Host.dll.": "[!!Véršò: Çòùld ñòt fïñd Véršò.Hòšt.dll. Šét \"véršò.hòštPàth\" ïñ šéttïñgš tò thé pàth òf ÿòùr bùïlt Véršò.Hòšt.dll.···!!]", + "Verso: Failed to open notebook: {0}": "[!!Véršò: Fàïléd tò òpéñ ñòtébòòk: {0}···!!]", + "Verso: Failed to start host process: {0}": "[!!Véršò: Fàïléd tò štàrt hòšt pròçéšš: {0}···!!]", + "Verso: Open a notebook to compare it with a baseline.": "[!!Véršò: Òpéñ à ñòtébòòk tò çòmpàré ït wïth à bàšélïñé.···!!]", + "Verso: installing the .NET runtime...": "[!!Véršò: ïñštàllïñg thé .ÑÉT rùñtïmé...···!!]", + "Verso: kernel restart aborted because the notebook snapshot could not be captured. Save and reopen the file.": "[!!Véršò: kérñél réštàrt àbòrtéd béçàùšé thé ñòtébòòk šñàpšhòt çòùld ñòt bé çàptùréd. Šàvé àñd réòpéñ thé fïlé.···!!]", + "Verso: kernel restart failed (notebook did not reopen): {0}. Close and reopen the notebook.": "[!!Véršò: kérñél réštàrt fàïléd (ñòtébòòk dïd ñòt réòpéñ): {0}. Çlòšé àñd réòpéñ thé ñòtébòòk.···!!]", + "Verso: the .NET runtime is installed. Reopen the notebook to continue.": "[!!Véršò: thé .ÑÉT rùñtïmé ïš ïñštàlléd. Réòpéñ thé ñòtébòòk tò çòñtïñùé.···!!]", + "Verso: the notebooks already open keep their current language. Reopen them to read them in the new one.": "[!!Véršò: thé ñòtébòòkš àlréàdÿ òpéñ kéép théïr çùrréñt làñgùàgé. Réòpéñ thém tò réàd thém ïñ thé ñéw òñé.···!!]", + "Yes/A table cell answering whether a setting is read-only.": "[!!Ýéš···!!]", + "branch/Says what kind of thing is listed, shown beside its name. A line of work in version control.": "[!!bràñçh···!!]", + "git could not read '{0}' at '{1}': {2}": "[!!gït çòùld ñòt réàd '{0}' àt '{1}': {2}···!!]", + "remote branch/A branch that lives on the server rather than on this machine.": "[!!rémòté bràñçh···!!]", + "tag/A name pinned to one point in a project's history.": "[!!tàg···!!]", + "unavailable/Said of a baseline that cannot be compared against, for a reason nothing here knows.": "[!!ùñàvàïlàblé···!!]", + "{0} Files/Names the kind of file a save box will accept. {0} is a format name such as CSV or HTML and is the same word in every language.": "[!!{0} Fïléš···!!]", + "{0} cell/Used when {0} is 1. Paired with the entry below.": "[!!{0} çéll···!!]", + "{0} cells/Used for every count other than 1. A language with one form for both translates this the same as the entry above.": "[!!{0} çéllš···!!]", + "{0} line/Used when {0} is 1. Paired with the entry below.": "[!!{0} lïñé···!!]", + "{0} lines/Used for every count other than 1. A language with one form for both translates this the same as the entry above.": "[!!{0} lïñéš···!!]" +} diff --git a/vscode/l10n/bundle.l10n.zh-cn.json b/vscode/l10n/bundle.l10n.zh-cn.json new file mode 100644 index 00000000..5228f4d0 --- /dev/null +++ b/vscode/l10n/bundle.l10n.zh-cn.json @@ -0,0 +1,96 @@ +{ + "'{0}' is not a known branch, tag, or commit.": "“{0}”不是已知的分支、标记或提交。", + "'{0}' is not tracked at '{1}'. Commit the file first, or pick a different ref.": "“{0}”在“{1}”处未被跟踪。请先提交该文件,或选择其他引用。", + "(not set)/Stands in a table cell for a setting that has no value yet.": "(未设置)", + "(unnamed)/Stands in for a branch or tag that has no name to show.": "(未命名)", + "**Cell {0}** [{1}] properties:": "**单元格 {0}** [{1}] 属性:", + "Adding parameter \"{0}\" ({1})": "正在添加参数“{0}”({1})", + "Adding {0} cell ({1})": "正在添加 {0} 单元格({1})", + "All Files/The entry in a save box that accepts any file at all.": "所有文件", + "Cell ({0}ms)/{0} is a number of milliseconds; ms is the unit and stays as written.": "单元格({0}ms)", + "Cell {0} has no configurable properties.": "单元格 {0} 没有可配置的属性。", + "Cell {0} not found. The notebook has {1}.": "找不到单元格 {0}。此笔记本有 {1}。", + "Cell {0}/A heading over one cell's code. {0} counts from 1.": "单元格 {0}", + "Changing cell {0} language to \"{1}\"": "正在将单元格 {0} 的语言更改为“{1}”", + "Changing cell {0} to type \"{1}\"": "正在将单元格 {0} 更改为类型“{1}”", + "Choose File.../Opens a box for picking another notebook to compare against. Keep the three dots, which mean a question follows.": "选择文件...", + "Compare notebook with...": "比较笔记本与...", + "Compare/The button that accepts the chosen file, in place of \"Open\".": "比较", + "Error: {0}/{0} is what the cell reported, in the language the kernel reported it.": "错误:{0}", + "Export failed: {0}": "导出失败:{0}", + "Exported to {0}": "已导出到 {0}", + "Extensions/Names the kind of file the box will accept.": "扩展", + "Git branch, tag, or commit SHA/Says what may be typed. Every term here is a version control term and stays as written.": "Git 分支、标记或提交 SHA", + "Git: Compare with Ref.../Opens a box for choosing a branch, tag, or commit. Git and Ref are version control terms and are not translated. Keep the three dots, which mean a question follows.": "Git:与 Ref 比较...", + "Git: HEAD/One of the things a notebook can be compared against. Git and HEAD are version control terms and are not translated.": "Git:HEAD", + "Images/Names the kind of file a save box will accept: pictures.": "图像", + "Input request failed: {0}": "输入请求失败:{0}", + "Install .NET Runtime/A button. .NET is a product name and stays as written.": "安装 .NET 运行时", + "Install Extension/The button that accepts the chosen file, in place of \"Open\".": "安装扩展", + "Kernel restart failed: the host process did not start ({0}). Close and reopen the notebook.": "内核重启失败:宿主进程未启动({0})。请关闭并重新打开笔记本。", + "Kernel restart failed: the notebook did not reopen ({0}).": "内核重启失败:笔记本未重新打开({0})。", + "Last Saved/One of the things a notebook can be compared against: the copy currently on disk.": "上次保存", + "Model error: {0}": "模型错误:{0}", + "Moving cell {0} to position {1}": "正在将单元格 {0} 移动到位置 {1}", + "Name/A table heading: what a variable is called.": "名称", + "No Verso notebook is currently open.": "当前没有打开的 Verso 笔记本。", + "No Verso notebook is currently open. Open a `.verso`, `.ipynb`, `.md`, or `.dib` file first.": "当前没有打开的 Verso 笔记本。请先打开 `.verso`、`.ipynb`、`.md` 或 `.dib` 文件。", + "No variables in scope. Run some cells first.": "作用域内没有变量。请先运行一些单元格。", + "No/A table cell answering whether a setting is read-only. The answer to a question, not the word for a number.": "否", + "Notebook Files/Names the kind of file the box will accept.": "笔记本文件", + "Notebook input/Asked when a running cell wants something typed and did not say what.": "笔记本输入", + "Property/A table heading: the name of one setting on a cell.": "属性", + "Provider: {0}/Names the extension a group of settings came from. {0} is its id.": "提供程序:{0}", + "Read-only/A table heading: whether a setting can be changed.": "只读", + "Remove cell": "移除单元格", + "Remove cell **{0}** from the notebook?": "要从笔记本中移除单元格 **{0}** 吗?", + "Remove parameter": "移除参数", + "Remove parameter **{0}** from the notebook?": "要从笔记本中移除参数 **{0}** 吗?", + "Removing cell {0}": "正在移除单元格 {0}", + "Removing parameter \"{0}\"": "正在移除参数“{0}”", + "Restart aborted: the notebook snapshot could not be captured ({0}).": "重启已中止:无法捕获笔记本快照({0})。", + "Running all cells": "正在运行所有单元格", + "Running all cells...": "正在运行所有单元格...", + "Running cell {0}": "正在运行单元格 {0}", + "Select a notebook for @verso/@verso is typed to address the assistant and stays as written.": "为 @verso 选择一个笔记本", + "Setting \"{0}\" on cell {1}": "正在设置单元格 {1} 的“{0}”", + "Setup Help/A button. It opens the page describing how to set Verso up.": "设置帮助", + "Switching layout to \"{0}\"": "正在将布局切换为“{0}”", + "The notebook file is not inside a git repository.": "笔记本文件不在 git 存储库中。", + "The notebook has no file on disk yet.": "此笔记本在磁盘上还没有文件。", + "The notebook is empty.": "此笔记本为空。", + "This comparison source is not available.": "此比较源不可用。", + "This notebook is not inside a git repository.": "此笔记本不在 git 存储库中。", + "Type a ref or commit.../The last entry in a list of branches and tags, for naming one that is not listed. Ref and commit are version control terms.": "键入 ref 或提交...", + "Type/A table heading: what kind of value a setting takes.": "类型", + "Type/A table heading: what kind of value a variable holds.": "类型", + "Unknown comparison source '{0}'.": "未知的比较源“{0}”。", + "Updating cell {0}": "正在更新单元格 {0}", + "Updating parameter \"{0}\"": "正在更新参数“{0}”", + "Usage: `/props ` (1-based). Example: `/props 2`/Shown when /props was typed without a cell number. Everything in backticks is typed and stays as written.": "用法:`/props `(从 1 开始)。示例:`/props 2`", + "Value/A table heading: what a setting is currently set to.": "值", + "Value/A table heading: what a variable currently holds.": "值", + "Verso host process exited ({0})": "Verso 宿主进程已退出({0})", + "Verso needs the .NET runtime (version {0} or later) to run notebooks, but a compatible installation was not found.": "Verso 需要 .NET 运行时(版本 {0} 或更高)才能运行笔记本,但未找到兼容的安装。", + "Verso: Could not create a scratch notebook: {0}": "Verso:无法创建临时笔记本:{0}", + "Verso: Could not find Verso.Host.dll. Set \"verso.hostPath\" in settings to the path of your built Verso.Host.dll.": "Verso:找不到 Verso.Host.dll。请在设置中将 \"verso.hostPath\" 设为你构建的 Verso.Host.dll 的路径。", + "Verso: Failed to open notebook: {0}": "Verso:无法打开笔记本:{0}", + "Verso: Failed to start host process: {0}": "Verso:无法启动宿主进程:{0}", + "Verso: Open a notebook to compare it with a baseline.": "Verso:请打开一个笔记本,以便与基线进行比较。", + "Verso: installing the .NET runtime...": "Verso:正在安装 .NET 运行时...", + "Verso: kernel restart aborted because the notebook snapshot could not be captured. Save and reopen the file.": "Verso:由于无法捕获笔记本快照,内核重启已中止。请保存并重新打开该文件。", + "Verso: kernel restart failed (notebook did not reopen): {0}. Close and reopen the notebook.": "Verso:内核重启失败(笔记本未重新打开):{0}。请关闭并重新打开笔记本。", + "Verso: the .NET runtime is installed. Reopen the notebook to continue.": "Verso:.NET 运行时已安装。请重新打开笔记本以继续。", + "Verso: the notebooks already open keep their current language. Reopen them to read them in the new one.": "Verso:已经打开的笔记本会保持当前语言。请重新打开它们,以使用新语言阅读。", + "Yes/A table cell answering whether a setting is read-only.": "是", + "branch/Says what kind of thing is listed, shown beside its name. A line of work in version control.": "分支", + "git could not read '{0}' at '{1}': {2}": "git 无法在“{1}”处读取“{0}”:{2}", + "remote branch/A branch that lives on the server rather than on this machine.": "远程分支", + "tag/A name pinned to one point in a project's history.": "标记", + "unavailable/Said of a baseline that cannot be compared against, for a reason nothing here knows.": "不可用", + "{0} Files/Names the kind of file a save box will accept. {0} is a format name such as CSV or HTML and is the same word in every language.": "{0} 文件", + "{0} cell/Used when {0} is 1. Paired with the entry below.": "{0} 个单元格", + "{0} cells/Used for every count other than 1. A language with one form for both translates this the same as the entry above.": "{0} 个单元格", + "{0} line/Used when {0} is 1. Paired with the entry below.": "{0} 行", + "{0} lines/Used for every count other than 1. A language with one form for both translates this the same as the entry above.": "{0} 行" +} diff --git a/vscode/package.json b/vscode/package.json index 42c649e5..ddafdd14 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -1,7 +1,7 @@ { "name": "verso-notebook", "displayName": "Verso Notebook", - "description": "Polyglot .NET notebooks with C#, F#, Python, JavaScript, TypeScript, PowerShell, SQL, and HTTP cells. One shared variable store across languages, IntelliSense, dashboards, Jupyter import, and GitHub Copilot integration.", + "description": "%extension.description%", "version": "1.0.0", "publisher": "datafication", "license": "MIT", @@ -73,22 +73,23 @@ "onCommand:verso.openInVerso" ], "main": "./dist/extension.js", + "l10n": "./l10n", "contributes": { "commands": [ { "command": "verso.newNotebook", - "title": "New Notebook", + "title": "%command.newNotebook.title%", "category": "Verso" }, { "command": "verso.compareWithBaseline", - "title": "Compare Notebook with...", + "title": "%command.compareWithBaseline.title%", "category": "Verso", "icon": "$(diff)" }, { "command": "verso.openInVerso", - "title": "Open as Verso Notebook", + "title": "%command.openInVerso.title%", "category": "Verso" } ], @@ -152,17 +153,17 @@ "verso.hostPath": { "type": "string", "default": "", - "description": "Path to the Verso.Host.dll. If empty, uses bundled host." + "description": "%configuration.hostPath.description%" }, "verso.dotnetPath": { "type": "string", "default": "", - "description": "Path to the 'dotnet' executable used to run notebooks. If empty, Verso reuses an installed .NET runtime (locating it via the .NET Install Tool when available) and falls back to 'dotnet' on PATH." + "description": "%configuration.dotnetPath.description%" }, "verso.python.interpreterPath": { "type": "string", "default": "", - "description": "Path to the Python interpreter used by Python cells. If empty, Verso discovers one from the active virtual environment, the workspace, and well-known install locations. Changes apply on next notebook open or kernel restart." + "description": "%configuration.python.interpreterPath.description%" }, "verso.python.autoInstall": { "type": "string", @@ -172,17 +173,38 @@ "off" ], "enumDescriptions": [ - "Ask before installing, listing the exact packages and the environment they go into. Declining runs the cell anyway.", - "Install known packages without asking. A package name Verso can only guess at is reported instead of installed.", - "Never scan a cell's imports and never install." + "%configuration.python.autoInstall.prompt%", + "%configuration.python.autoInstall.auto%", + "%configuration.python.autoInstall.off%" ], "default": "prompt", - "description": "What happens when a Python cell imports a package the environment does not have. Changes apply on next notebook open or kernel restart." + "description": "%configuration.python.autoInstall.description%" }, "verso.python.useUv": { "type": "boolean", "default": true, - "description": "Use the uv tool to install packages and create Python environments when it is on PATH. When off, pip and the standard library venv module are used. Changes apply on next notebook open or kernel restart." + "description": "%configuration.python.useUv.description%" + }, + "verso.language": { + "type": "string", + "enum": [ + "auto", + "en", + "de", + "es", + "ja", + "zh-Hans" + ], + "enumItemLabels": [ + "%configuration.language.auto%", + "English", + "Deutsch", + "Español", + "日本語", + "简体中文" + ], + "default": "auto", + "description": "%configuration.language.description%" }, "verso.extensionsPath": { "type": "array", @@ -190,17 +212,17 @@ "type": "string" }, "default": [], - "description": "Directories of third-party Verso extension assemblies to load on notebook open. Each entry is one directory path. Changes apply on next notebook open or kernel restart." + "description": "%configuration.extensionsPath.description%" }, "verso.preserveOriginalFormat": { "type": "boolean", "default": false, - "description": "When opening an .ipynb file, save changes back to .ipynb instead of converting to .verso. Cell outputs are preserved. Default off keeps the existing convert-on-save behavior. Markdown (.md) notebooks always save back to .md regardless of this setting." + "description": "%configuration.preserveOriginalFormat.description%" }, "verso.showOpenInVersoMenu": { "type": "boolean", "default": true, - "description": "Show the 'Open as Verso Notebook' entry in the Explorer context menu for Markdown (.md) files. Disabling this hides the menu entry only; .md files can still be opened in Verso through the editor's 'Reopen Editor With...' picker." + "description": "%configuration.showOpenInVersoMenu.description%" } } }, @@ -209,24 +231,24 @@ "id": "verso.copilot.notebook", "name": "verso", "fullName": "Verso Notebook", - "description": "Create, edit, run, and explore Verso notebook cells", + "description": "%chat.participant.description%", "isSticky": true, "commands": [ { "name": "cells", - "description": "List all cells in the active notebook" + "description": "%chat.cells.description%" }, { "name": "run", - "description": "Run all cells in the active notebook" + "description": "%chat.run.description%" }, { "name": "vars", - "description": "Show variables in scope" + "description": "%chat.vars.description%" }, { "name": "props", - "description": "Show properties for a cell" + "description": "%chat.props.description%" } ] } @@ -235,7 +257,7 @@ { "name": "verso_listCells", "tags": ["verso", "notebook", "cells"], - "displayName": "List Cells", + "displayName": "%tool.listCells.displayName%", "modelDescription": "List all cells in the active Verso notebook, showing each cell's source code, language, and outputs.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -246,7 +268,7 @@ { "name": "verso_addCell", "tags": ["verso", "notebook", "cells"], - "displayName": "Add Cell", + "displayName": "%tool.addCell.displayName%", "modelDescription": "Add a new cell to the Verso notebook. The 'type' field determines the cell kind. For code cells, also set 'language' to the kernel. For non-code types (markdown, html, mermaid, etc.), only type is needed. Refer to the system prompt for available cell types and languages.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -275,7 +297,7 @@ { "name": "verso_updateCell", "tags": ["verso", "notebook", "cells"], - "displayName": "Update Cell", + "displayName": "%tool.updateCell.displayName%", "modelDescription": "Update the source code of an existing cell. Use the 1-based cell number (call verso_listCells first to find it).", "canBeReferencedInPrompt": false, "inputSchema": { @@ -296,7 +318,7 @@ { "name": "verso_removeCell", "tags": ["verso", "notebook", "cells"], - "displayName": "Remove Cell", + "displayName": "%tool.removeCell.displayName%", "modelDescription": "Remove a cell from the notebook by its 1-based cell number.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -313,7 +335,7 @@ { "name": "verso_runCell", "tags": ["verso", "notebook", "execution"], - "displayName": "Run Cell", + "displayName": "%tool.runCell.displayName%", "modelDescription": "Execute a specific cell by its 1-based cell number and return the output.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -330,7 +352,7 @@ { "name": "verso_runAll", "tags": ["verso", "notebook", "execution"], - "displayName": "Run All Cells", + "displayName": "%tool.runAll.displayName%", "modelDescription": "Execute all cells in the notebook sequentially and return results.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -341,7 +363,7 @@ { "name": "verso_listVariables", "tags": ["verso", "notebook", "variables"], - "displayName": "List Variables", + "displayName": "%tool.listVariables.displayName%", "modelDescription": "List all variables currently in scope in the notebook kernel, showing name, type, and value preview.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -352,7 +374,7 @@ { "name": "verso_inspectVariable", "tags": ["verso", "notebook", "variables"], - "displayName": "Inspect Variable", + "displayName": "%tool.inspectVariable.displayName%", "modelDescription": "Get the detailed value of a specific variable by name.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -369,7 +391,7 @@ { "name": "verso_getLanguages", "tags": ["verso", "notebook"], - "displayName": "Get Languages", + "displayName": "%tool.getLanguages.displayName%", "modelDescription": "List the available languages/kernels for the active notebook.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -380,7 +402,7 @@ { "name": "verso_listParameters", "tags": ["verso", "notebook", "parameters"], - "displayName": "List Parameters", + "displayName": "%tool.listParameters.displayName%", "modelDescription": "List all parameters defined in the notebook. Parameters are named, typed values (string, int, float, bool, date, datetime) that can be set before execution to control notebook behavior.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -391,7 +413,7 @@ { "name": "verso_addParameter", "tags": ["verso", "notebook", "parameters"], - "displayName": "Add Parameter", + "displayName": "%tool.addParameter.displayName%", "modelDescription": "Add a new parameter to the notebook. Supported types: string, int, float, bool, date (yyyy-MM-dd), datetime (ISO 8601). A parameters cell is automatically created if one does not exist.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -425,7 +447,7 @@ { "name": "verso_updateParameter", "tags": ["verso", "notebook", "parameters"], - "displayName": "Update Parameter", + "displayName": "%tool.updateParameter.displayName%", "modelDescription": "Update an existing parameter's properties. Only the fields you provide will be changed. Use verso_listParameters first to see current parameters.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -459,7 +481,7 @@ { "name": "verso_removeParameter", "tags": ["verso", "notebook", "parameters"], - "displayName": "Remove Parameter", + "displayName": "%tool.removeParameter.displayName%", "modelDescription": "Remove a parameter from the notebook by name. This also removes the corresponding variable from the kernel.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -476,7 +498,7 @@ { "name": "verso_getCellProperties", "tags": ["verso", "notebook", "properties"], - "displayName": "Get Cell Properties", + "displayName": "%tool.getCellProperties.displayName%", "modelDescription": "Get the configurable properties for a specific cell. Properties are extension-provided settings like visibility, formatting, or tags. Call this before verso_updateCellProperty to discover available properties and their current values. The response includes providerExtensionId values needed for updates.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -493,7 +515,7 @@ { "name": "verso_updateCellProperty", "tags": ["verso", "notebook", "properties"], - "displayName": "Update Cell Property", + "displayName": "%tool.updateCellProperty.displayName%", "modelDescription": "Update a property on a cell. Call verso_getCellProperties first to discover the available properties, their field types, and the providerExtensionId needed for this call. For Select fields, use the option value (not display name). For Toggle fields, use 'true' or 'false'.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -522,7 +544,7 @@ { "name": "verso_listLayouts", "tags": ["verso", "notebook", "layout"], - "displayName": "List Layouts", + "displayName": "%tool.listLayouts.displayName%", "modelDescription": "List the layouts registered for this notebook (for example Notebook or Dashboard). Each entry includes its displayName, the extensionId and layoutId needed by verso_switchLayout, and whether it is currently active. Call this before verso_switchLayout to discover the qualified identity of the target layout.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -533,7 +555,7 @@ { "name": "verso_switchLayout", "tags": ["verso", "notebook", "layout"], - "displayName": "Switch Layout", + "displayName": "%tool.switchLayout.displayName%", "modelDescription": "Switch the notebook's active layout. Call verso_listLayouts first to get the extensionId and layoutId of the target layout. The choice is saved with the notebook. Some layouts (such as Dashboard) present cells differently, for example output-only with grid positions.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -554,7 +576,7 @@ { "name": "verso_moveCell", "tags": ["verso", "notebook", "cells"], - "displayName": "Move Cell", + "displayName": "%tool.moveCell.displayName%", "modelDescription": "Move a cell to a different position in the notebook. Both numbers are 1-based. Call verso_listCells first to confirm the current order.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -575,7 +597,7 @@ { "name": "verso_changeCellType", "tags": ["verso", "notebook", "cells"], - "displayName": "Change Cell Type", + "displayName": "%tool.changeCellType.displayName%", "modelDescription": "Change a cell's type (for example code, markdown, html, mermaid). Call verso_getLanguages or verso_listCells to see available types. Changing the type clears the cell's outputs. For code cells the language is set automatically; use verso_changeCellLanguage to choose a different one.", "canBeReferencedInPrompt": false, "inputSchema": { @@ -596,7 +618,7 @@ { "name": "verso_changeCellLanguage", "tags": ["verso", "notebook", "cells"], - "displayName": "Change Cell Language", + "displayName": "%tool.changeCellLanguage.displayName%", "modelDescription": "Change the kernel language of a code cell (for example csharp, sql, python). Call verso_getLanguages to list valid languages. Changing the language clears the cell's outputs.", "canBeReferencedInPrompt": false, "inputSchema": { diff --git a/vscode/package.nls.de.json b/vscode/package.nls.de.json new file mode 100644 index 00000000..bcab66a1 --- /dev/null +++ b/vscode/package.nls.de.json @@ -0,0 +1,44 @@ +{ + "chat.cells.description": "Alle Zellen im aktiven Notebook auflisten", + "chat.participant.description": "Verso-Notebookzellen erstellen, bearbeiten, ausführen und erkunden", + "chat.props.description": "Eigenschaften einer Zelle anzeigen", + "chat.run.description": "Alle Zellen im aktiven Notebook ausführen", + "chat.vars.description": "Variablen im Gültigkeitsbereich anzeigen", + "command.compareWithBaseline.title": "Notebook vergleichen mit...", + "command.newNotebook.title": "Neues Notebook", + "command.openInVerso.title": "Als Verso Notebook öffnen", + "configuration.dotnetPath.description": "Pfad zur ausführbaren Datei 'dotnet', mit der Notebooks ausgeführt werden. Ohne Angabe verwendet Verso eine installierte .NET-Laufzeit (und findet sie, sofern vorhanden, über das .NET Install Tool) und greift ersatzweise auf 'dotnet' in PATH zurück.", + "configuration.extensionsPath.description": "Verzeichnisse mit Verso-Erweiterungsassemblys von Drittanbietern, die beim Öffnen eines Notebooks geladen werden. Ein Verzeichnispfad je Eintrag. Änderungen werden beim nächsten Öffnen eines Notebooks oder Neustart des Kernels wirksam.", + "configuration.hostPath.description": "Pfad zur Verso.Host.dll. Ohne Angabe wird der mitgelieferte Host verwendet.", + "configuration.language.auto": "Automatisch erkennen", + "configuration.language.description": "Sprache der Notebook-Oberfläche und der Kernelmeldungen. Automatisch erkennen folgt der Anzeigesprache von VS Code. Menüeinträge, Befehlsnamen und Beschreibungen von Einstellungen folgen immer der Anzeigesprache von VS Code und werden von dieser Einstellung nicht beeinflusst. Änderungen werden beim nächsten Öffnen eines Notebooks wirksam.", + "configuration.preserveOriginalFormat.description": "Beim Öffnen einer .ipynb-Datei Änderungen nach .ipynb zurückschreiben, statt nach .verso zu konvertieren. Zellenausgaben bleiben erhalten. Standardmäßig aus, wodurch das bisherige Konvertieren beim Speichern erhalten bleibt. Markdown-Notebooks (.md) werden unabhängig von dieser Einstellung immer nach .md zurückgeschrieben.", + "configuration.python.autoInstall.auto": "Bekannte Pakete ohne Nachfrage installieren. Ein Paketname, den Verso nur erraten kann, wird gemeldet statt installiert.", + "configuration.python.autoInstall.description": "Was geschieht, wenn eine Python-Zelle ein Paket importiert, das in der Umgebung fehlt. Änderungen werden beim nächsten Öffnen eines Notebooks oder Neustart des Kernels wirksam.", + "configuration.python.autoInstall.off": "Die Importe einer Zelle nie durchsuchen und nie installieren.", + "configuration.python.autoInstall.prompt": "Vor dem Installieren nachfragen und dabei die genauen Pakete und die Zielumgebung nennen. Bei Ablehnung wird die Zelle trotzdem ausgeführt.", + "configuration.python.interpreterPath.description": "Pfad zum Python-Interpreter, den Python-Zellen verwenden. Ohne Angabe findet Verso einen über die aktive virtuelle Umgebung, den Arbeitsordner und die üblichen Installationsorte. Änderungen werden beim nächsten Öffnen eines Notebooks oder Neustart des Kernels wirksam.", + "configuration.python.useUv.description": "Das Werkzeug uv zum Installieren von Paketen und Erstellen von Python-Umgebungen verwenden, wenn es in PATH liegt. Ist die Einstellung aus, werden pip und das Standardbibliotheksmodul venv verwendet. Änderungen werden beim nächsten Öffnen eines Notebooks oder Neustart des Kernels wirksam.", + "configuration.showOpenInVersoMenu.description": "Den Eintrag 'Als Verso Notebook öffnen' im Kontextmenü des Explorers für Markdown-Dateien (.md) anzeigen. Wird dies deaktiviert, verschwindet nur der Menüeintrag; .md-Dateien lassen sich weiterhin über die Auswahl 'Editor erneut öffnen mit...' in Verso öffnen.", + "extension.description": "Polyglotte .NET-Notebooks mit Zellen in C#, F#, Python, JavaScript, TypeScript, PowerShell, SQL und HTTP. Ein gemeinsamer Variablenspeicher über alle Sprachen hinweg, IntelliSense, Dashboards, Jupyter-Import und Anbindung an GitHub Copilot.", + "tool.addCell.displayName": "Zelle hinzufügen", + "tool.addParameter.displayName": "Parameter hinzufügen", + "tool.changeCellLanguage.displayName": "Zellensprache ändern", + "tool.changeCellType.displayName": "Zellentyp ändern", + "tool.getCellProperties.displayName": "Zelleneigenschaften abrufen", + "tool.getLanguages.displayName": "Sprachen abrufen", + "tool.inspectVariable.displayName": "Variable untersuchen", + "tool.listCells.displayName": "Zellen auflisten", + "tool.listLayouts.displayName": "Layouts auflisten", + "tool.listParameters.displayName": "Parameter auflisten", + "tool.listVariables.displayName": "Variablen auflisten", + "tool.moveCell.displayName": "Zelle verschieben", + "tool.removeCell.displayName": "Zelle entfernen", + "tool.removeParameter.displayName": "Parameter entfernen", + "tool.runAll.displayName": "Alle Zellen ausführen", + "tool.runCell.displayName": "Zelle ausführen", + "tool.switchLayout.displayName": "Layout wechseln", + "tool.updateCell.displayName": "Zelle aktualisieren", + "tool.updateCellProperty.displayName": "Zelleneigenschaft aktualisieren", + "tool.updateParameter.displayName": "Parameter aktualisieren" +} diff --git a/vscode/package.nls.es.json b/vscode/package.nls.es.json new file mode 100644 index 00000000..681398ed --- /dev/null +++ b/vscode/package.nls.es.json @@ -0,0 +1,44 @@ +{ + "chat.cells.description": "Enumera todas las celdas del cuaderno activo", + "chat.participant.description": "Crea, edita, ejecuta y explora celdas de cuadernos de Verso", + "chat.props.description": "Muestra las propiedades de una celda", + "chat.run.description": "Ejecuta todas las celdas del cuaderno activo", + "chat.vars.description": "Muestra las variables en ámbito", + "command.compareWithBaseline.title": "Comparar el cuaderno con...", + "command.newNotebook.title": "Nuevo cuaderno", + "command.openInVerso.title": "Abrir como Verso Notebook", + "configuration.dotnetPath.description": "Ruta al ejecutable 'dotnet' que se usa para ejecutar los cuadernos. Si está vacío, Verso reutiliza un runtime de .NET instalado (localizándolo con .NET Install Tool cuando está disponible) y recurre a 'dotnet' en PATH.", + "configuration.extensionsPath.description": "Directorios de ensamblados de extensión de Verso de terceros que se cargan al abrir un cuaderno. Cada entrada es la ruta de un directorio. Los cambios se aplican al abrir el siguiente cuaderno o al reiniciar el kernel.", + "configuration.hostPath.description": "Ruta a Verso.Host.dll. Si está vacío, se usa el host incluido.", + "configuration.language.auto": "Detección automática", + "configuration.language.description": "Idioma de la interfaz del cuaderno y de los mensajes del kernel. La detección automática sigue el idioma de presentación de VS Code. Las entradas de menú, los nombres de comando y las descripciones de configuración siguen siempre el idioma de presentación de VS Code y esta opción no les afecta. Los cambios se aplican al abrir el siguiente cuaderno.", + "configuration.preserveOriginalFormat.description": "Al abrir un archivo .ipynb, guarda los cambios de nuevo en .ipynb en lugar de convertirlo a .verso. Las salidas de las celdas se conservan. Deshabilitado de forma predeterminada, se mantiene el comportamiento actual de convertir al guardar. Los cuadernos Markdown (.md) siempre se guardan de nuevo en .md, independientemente de esta opción.", + "configuration.python.autoInstall.auto": "Instala los paquetes conocidos sin preguntar. Si Verso solo puede adivinar el nombre de un paquete, se informa en lugar de instalarlo.", + "configuration.python.autoInstall.description": "Qué ocurre cuando una celda de Python importa un paquete que el entorno no tiene. Los cambios se aplican al abrir el siguiente cuaderno o al reiniciar el kernel.", + "configuration.python.autoInstall.off": "No analiza nunca las importaciones de una celda ni instala nada.", + "configuration.python.autoInstall.prompt": "Pregunta antes de instalar, indicando los paquetes exactos y el entorno al que van. Si se rechaza, la celda se ejecuta igualmente.", + "configuration.python.interpreterPath.description": "Ruta al intérprete de Python que usan las celdas de Python. Si está vacío, Verso detecta uno a partir del entorno virtual activo, del espacio de trabajo y de las ubicaciones de instalación habituales. Los cambios se aplican al abrir el siguiente cuaderno o al reiniciar el kernel.", + "configuration.python.useUv.description": "Usa la herramienta uv para instalar paquetes y crear entornos de Python cuando está en PATH. Si se deshabilita, se usan pip y el módulo venv de la biblioteca estándar. Los cambios se aplican al abrir el siguiente cuaderno o al reiniciar el kernel.", + "configuration.showOpenInVersoMenu.description": "Muestra la entrada 'Abrir como Verso Notebook' en el menú contextual del Explorador para los archivos Markdown (.md). Deshabilitarlo solo oculta la entrada de menú; los archivos .md se pueden seguir abriendo en Verso con el selector 'Volver a abrir el editor con...' del editor.", + "extension.description": "Cuadernos políglotas de .NET con celdas de C#, F#, Python, JavaScript, TypeScript, PowerShell, SQL y HTTP. Un único almacén de variables compartido entre lenguajes, IntelliSense, dashboards, importación desde Jupyter e integración con GitHub Copilot.", + "tool.addCell.displayName": "Añadir celda", + "tool.addParameter.displayName": "Añadir parámetro", + "tool.changeCellLanguage.displayName": "Cambiar el lenguaje de la celda", + "tool.changeCellType.displayName": "Cambiar el tipo de celda", + "tool.getCellProperties.displayName": "Obtener las propiedades de la celda", + "tool.getLanguages.displayName": "Obtener los lenguajes", + "tool.inspectVariable.displayName": "Inspeccionar una variable", + "tool.listCells.displayName": "Enumerar las celdas", + "tool.listLayouts.displayName": "Enumerar los diseños", + "tool.listParameters.displayName": "Enumerar los parámetros", + "tool.listVariables.displayName": "Enumerar las variables", + "tool.moveCell.displayName": "Mover la celda", + "tool.removeCell.displayName": "Quitar la celda", + "tool.removeParameter.displayName": "Quitar el parámetro", + "tool.runAll.displayName": "Ejecutar todas las celdas", + "tool.runCell.displayName": "Ejecutar la celda", + "tool.switchLayout.displayName": "Cambiar el diseño", + "tool.updateCell.displayName": "Actualizar la celda", + "tool.updateCellProperty.displayName": "Actualizar una propiedad de la celda", + "tool.updateParameter.displayName": "Actualizar el parámetro" +} diff --git a/vscode/package.nls.ja.json b/vscode/package.nls.ja.json new file mode 100644 index 00000000..954bf93f --- /dev/null +++ b/vscode/package.nls.ja.json @@ -0,0 +1,44 @@ +{ + "chat.cells.description": "アクティブなノートブックのすべてのセルを一覧表示します", + "chat.participant.description": "Verso ノートブックのセルを作成、編集、実行、確認します", + "chat.props.description": "セルのプロパティを表示します", + "chat.run.description": "アクティブなノートブックのすべてのセルを実行します", + "chat.vars.description": "スコープ内の変数を表示します", + "command.compareWithBaseline.title": "ノートブックを比較...", + "command.newNotebook.title": "新しいノートブック", + "command.openInVerso.title": "Verso ノートブックとして開く", + "configuration.dotnetPath.description": "ノートブックの実行に使う 'dotnet' 実行ファイルのパス。空の場合、Verso はインストール済みの .NET ランタイムを再利用し (利用できるときは .NET Install Tool で探します)、見つからなければ PATH 上の 'dotnet' を使います。", + "configuration.extensionsPath.description": "ノートブックを開いたときに読み込む、サードパーティの Verso 拡張機能アセンブリのディレクトリ。1 項目につき 1 つのディレクトリパスを指定します。変更は次にノートブックを開いたとき、またはカーネルの再起動時に反映されます。", + "configuration.hostPath.description": "Verso.Host.dll のパス。空の場合は同梱のホストを使います。", + "configuration.language.auto": "自動検出", + "configuration.language.description": "ノートブックのインターフェイスとカーネルのメッセージの言語。自動検出は VS Code の表示言語に従います。メニュー項目、コマンド名、設定の説明は常に VS Code の表示言語に従い、この設定の影響を受けません。変更は次にノートブックを開いたときに反映されます。", + "configuration.preserveOriginalFormat.description": ".ipynb ファイルを開いたとき、.verso に変換せず .ipynb に書き戻します。セルの出力は保持されます。既定では無効で、保存時に変換する従来の動作になります。Markdown (.md) のノートブックは、この設定にかかわらず常に .md に保存されます。", + "configuration.python.autoInstall.auto": "既知のパッケージを確認なしでインストールします。Verso が推測するしかないパッケージ名は、インストールせずに報告します。", + "configuration.python.autoInstall.description": "Python セルが、環境にないパッケージを import したときの動作。変更は次にノートブックを開いたとき、またはカーネルの再起動時に反映されます。", + "configuration.python.autoInstall.off": "セルの import を調べず、インストールもしません。", + "configuration.python.autoInstall.prompt": "インストールの前に、対象のパッケージとインストール先の環境を示して確認します。断った場合もセルはそのまま実行されます。", + "configuration.python.interpreterPath.description": "Python セルが使うインタープリターのパス。空の場合、Verso は現在有効な仮想環境、ワークスペース、既知のインストール先から探します。変更は次にノートブックを開いたとき、またはカーネルの再起動時に反映されます。", + "configuration.python.useUv.description": "uv が PATH にあるとき、パッケージのインストールと Python 環境の作成に uv を使います。無効の場合は pip と標準ライブラリの venv モジュールを使います。変更は次にノートブックを開いたとき、またはカーネルの再起動時に反映されます。", + "configuration.showOpenInVersoMenu.description": "エクスプローラーの Markdown (.md) ファイルのコンテキストメニューに「Verso ノートブックとして開く」を表示します。無効にするとメニュー項目が消えるだけで、.md ファイルはエディターの「エディターを再度開く...」からも Verso で開けます。", + "extension.description": "C#、F#、Python、JavaScript、TypeScript、PowerShell、SQL、HTTP のセルを扱える多言語対応の .NET ノートブック。言語をまたいで共有される変数ストア、IntelliSense、ダッシュボード、Jupyter のインポート、GitHub Copilot との連携を備えています。", + "tool.addCell.displayName": "セルを追加", + "tool.addParameter.displayName": "パラメーターを追加", + "tool.changeCellLanguage.displayName": "セルの言語を変更", + "tool.changeCellType.displayName": "セルタイプを変更", + "tool.getCellProperties.displayName": "セルのプロパティを取得", + "tool.getLanguages.displayName": "言語を取得", + "tool.inspectVariable.displayName": "変数を調べる", + "tool.listCells.displayName": "セルを一覧表示", + "tool.listLayouts.displayName": "レイアウトを一覧表示", + "tool.listParameters.displayName": "パラメーターを一覧表示", + "tool.listVariables.displayName": "変数を一覧表示", + "tool.moveCell.displayName": "セルを移動", + "tool.removeCell.displayName": "セルを削除", + "tool.removeParameter.displayName": "パラメーターを削除", + "tool.runAll.displayName": "すべてのセルを実行", + "tool.runCell.displayName": "セルを実行", + "tool.switchLayout.displayName": "レイアウトを切り替え", + "tool.updateCell.displayName": "セルを更新", + "tool.updateCellProperty.displayName": "セルのプロパティを更新", + "tool.updateParameter.displayName": "パラメーターを更新" +} diff --git a/vscode/package.nls.json b/vscode/package.nls.json new file mode 100644 index 00000000..5386c5ab --- /dev/null +++ b/vscode/package.nls.json @@ -0,0 +1,116 @@ +{ + "extension.description": { + "message": "Polyglot .NET notebooks with C#, F#, Python, JavaScript, TypeScript, PowerShell, SQL, and HTTP cells. One shared variable store across languages, IntelliSense, dashboards, Jupyter import, and GitHub Copilot integration.", + "comment": [ + "The blurb under the extension's name in the Marketplace.", + "Every language name and product name here is written the same in every language." + ] + }, + + "command.newNotebook.title": { + "message": "New Notebook", + "comment": ["A Command Palette entry. It creates a notebook that has no file yet."] + }, + "command.compareWithBaseline.title": { + "message": "Compare Notebook with...", + "comment": [ + "A Command Palette entry. It asks which earlier copy of the notebook to compare against.", + "Keep the three dots, which mean a question follows." + ] + }, + "command.openInVerso.title": { + "message": "Open as Verso Notebook", + "comment": [ + "Offered when right-clicking a Markdown file. It opens that file as a notebook.", + "\"Verso Notebook\" is the product's name and is written the same in every language." + ] + }, + + "configuration.hostPath.description": { + "message": "Path to the Verso.Host.dll. If empty, uses bundled host.", + "comment": ["Verso.Host.dll is a file name and is written the same in every language."] + }, + "configuration.dotnetPath.description": { + "message": "Path to the 'dotnet' executable used to run notebooks. If empty, Verso reuses an installed .NET runtime (locating it via the .NET Install Tool when available) and falls back to 'dotnet' on PATH.", + "comment": [ + "'dotnet', .NET, and PATH are written the same in every language.", + "\".NET Install Tool\" is the name of another extension; leave it as written so it can be found." + ] + }, + "configuration.python.interpreterPath.description": { + "message": "Path to the Python interpreter used by Python cells. If empty, Verso discovers one from the active virtual environment, the workspace, and well-known install locations. Changes apply on next notebook open or kernel restart." + }, + "configuration.python.autoInstall.description": { + "message": "What happens when a Python cell imports a package the environment does not have. Changes apply on next notebook open or kernel restart." + }, + "configuration.python.autoInstall.prompt": { + "message": "Ask before installing, listing the exact packages and the environment they go into. Declining runs the cell anyway." + }, + "configuration.python.autoInstall.auto": { + "message": "Install known packages without asking. A package name Verso can only guess at is reported instead of installed." + }, + "configuration.python.autoInstall.off": { + "message": "Never scan a cell's imports and never install." + }, + "configuration.python.useUv.description": { + "message": "Use the uv tool to install packages and create Python environments when it is on PATH. When off, pip and the standard library venv module are used. Changes apply on next notebook open or kernel restart.", + "comment": ["uv, pip, venv, and PATH are names typed at a keyboard and stay as written."] + }, + "configuration.language.description": { + "message": "Language for the notebook interface and kernel messages. Auto-detect follows the VS Code display language. Menu entries, command names, and setting descriptions always follow the VS Code display language and are not affected by this setting. Changes apply on next notebook open.", + "comment": [ + "\"Auto-detect\" here names the first entry in this setting's own list; translate it the same way in both places." + ] + }, + "configuration.language.auto": { + "message": "Auto-detect", + "comment": [ + "The first entry in the language list: take the language from the editor rather than choosing one.", + "The other entries are the names languages call themselves, so they are the same in every list and are not translated." + ] + }, + "configuration.extensionsPath.description": { + "message": "Directories of third-party Verso extension assemblies to load on notebook open. Each entry is one directory path. Changes apply on next notebook open or kernel restart." + }, + "configuration.preserveOriginalFormat.description": { + "message": "When opening an .ipynb file, save changes back to .ipynb instead of converting to .verso. Cell outputs are preserved. Default off keeps the existing convert-on-save behavior. Markdown (.md) notebooks always save back to .md regardless of this setting.", + "comment": ["File extensions stay exactly as spelled."] + }, + "configuration.showOpenInVersoMenu.description": { + "message": "Show the 'Open as Verso Notebook' entry in the Explorer context menu for Markdown (.md) files. Disabling this hides the menu entry only; .md files can still be opened in Verso through the editor's 'Reopen Editor With...' picker.", + "comment": [ + "'Open as Verso Notebook' names the menu entry above; translate it the same way in both places.", + "'Reopen Editor With...' is the editor's own wording; use whatever the editor calls it in this language." + ] + }, + + "chat.participant.description": { + "message": "Create, edit, run, and explore Verso notebook cells", + "comment": ["Shown beside @verso in the chat view. No full stop, matching the other participants."] + }, + "chat.cells.description": "List all cells in the active notebook", + "chat.run.description": "Run all cells in the active notebook", + "chat.vars.description": "Show variables in scope", + "chat.props.description": "Show properties for a cell", + + "tool.listCells.displayName": "List Cells", + "tool.addCell.displayName": "Add Cell", + "tool.updateCell.displayName": "Update Cell", + "tool.removeCell.displayName": "Remove Cell", + "tool.runCell.displayName": "Run Cell", + "tool.runAll.displayName": "Run All Cells", + "tool.listVariables.displayName": "List Variables", + "tool.inspectVariable.displayName": "Inspect Variable", + "tool.getLanguages.displayName": "Get Languages", + "tool.listParameters.displayName": "List Parameters", + "tool.addParameter.displayName": "Add Parameter", + "tool.updateParameter.displayName": "Update Parameter", + "tool.removeParameter.displayName": "Remove Parameter", + "tool.getCellProperties.displayName": "Get Cell Properties", + "tool.updateCellProperty.displayName": "Update Cell Property", + "tool.listLayouts.displayName": "List Layouts", + "tool.switchLayout.displayName": "Switch Layout", + "tool.moveCell.displayName": "Move Cell", + "tool.changeCellType.displayName": "Change Cell Type", + "tool.changeCellLanguage.displayName": "Change Cell Language" +} diff --git a/vscode/package.nls.qps-ploc.json b/vscode/package.nls.qps-ploc.json new file mode 100644 index 00000000..af91302c --- /dev/null +++ b/vscode/package.nls.qps-ploc.json @@ -0,0 +1,44 @@ +{ + "chat.cells.description": "[!!Lïšt àll çéllš ïñ thé àçtïvé ñòtébòòk···!!]", + "chat.participant.description": "[!!Çréàté, édït, rùñ, àñd éxplòré Véršò ñòtébòòk çéllš···!!]", + "chat.props.description": "[!!Šhòw pròpértïéš fòr à çéll···!!]", + "chat.run.description": "[!!Rùñ àll çéllš ïñ thé àçtïvé ñòtébòòk···!!]", + "chat.vars.description": "[!!Šhòw vàrïàbléš ïñ šçòpé···!!]", + "command.compareWithBaseline.title": "[!!Çòmpàré Ñòtébòòk wïth...···!!]", + "command.newNotebook.title": "[!!Ñéw Ñòtébòòk···!!]", + "command.openInVerso.title": "[!!Òpéñ àš Véršò Ñòtébòòk···!!]", + "configuration.dotnetPath.description": "[!!Pàth tò thé 'dòtñét' éxéçùtàblé ùšéd tò rùñ ñòtébòòkš. Ïf émptÿ, Véršò réùšéš àñ ïñštàlléd .ÑÉT rùñtïmé (lòçàtïñg ït vïà thé .ÑÉT Ïñštàll Tòòl whéñ àvàïlàblé) àñd fàllš bàçk tò 'dòtñét' òñ PÀTH.···!!]", + "configuration.extensionsPath.description": "[!!Dïréçtòrïéš òf thïrd-pàrtÿ Véršò éxtéñšïòñ àššémblïéš tò lòàd òñ ñòtébòòk òpéñ. Éàçh éñtrÿ ïš òñé dïréçtòrÿ pàth. Çhàñgéš àpplÿ òñ ñéxt ñòtébòòk òpéñ òr kérñél réštàrt.···!!]", + "configuration.hostPath.description": "[!!Pàth tò thé Véršò.Hòšt.dll. Ïf émptÿ, ùšéš bùñdléd hòšt.···!!]", + "configuration.language.auto": "[!!Àùtò-détéçt···!!]", + "configuration.language.description": "[!!Làñgùàgé fòr thé ñòtébòòk ïñtérfàçé àñd kérñél méššàgéš. Àùtò-détéçt fòllòwš thé VŠ Çòdé dïšplàÿ làñgùàgé. Méñù éñtrïéš, çòmmàñd ñàméš, àñd šéttïñg déšçrïptïòñš àlwàÿš fòllòw thé VŠ Çòdé dïšplàÿ làñgùàgé àñd àré ñòt àfféçtéd bÿ thïš šéttïñg. Çhàñgéš àpplÿ òñ ñéxt ñòtébòòk òpéñ.···!!]", + "configuration.preserveOriginalFormat.description": "[!!Whéñ òpéñïñg àñ .ïpÿñb fïlé, šàvé çhàñgéš bàçk tò .ïpÿñb ïñštéàd òf çòñvértïñg tò .véršò. Çéll òùtpùtš àré préšérvéd. Défàùlt òff kéépš thé éxïštïñg çòñvért-òñ-šàvé béhàvïòr. Màrkdòwñ (.md) ñòtébòòkš àlwàÿš šàvé bàçk tò .md régàrdléšš òf thïš šéttïñg.···!!]", + "configuration.python.autoInstall.auto": "[!!Ïñštàll kñòwñ pàçkàgéš wïthòùt àškïñg. À pàçkàgé ñàmé Véršò çàñ òñlÿ gùéšš àt ïš répòrtéd ïñštéàd òf ïñštàlléd.···!!]", + "configuration.python.autoInstall.description": "[!!Whàt hàppéñš whéñ à Pÿthòñ çéll ïmpòrtš à pàçkàgé thé éñvïròñméñt dòéš ñòt hàvé. Çhàñgéš àpplÿ òñ ñéxt ñòtébòòk òpéñ òr kérñél réštàrt.···!!]", + "configuration.python.autoInstall.off": "[!!Ñévér šçàñ à çéll'š ïmpòrtš àñd ñévér ïñštàll.···!!]", + "configuration.python.autoInstall.prompt": "[!!Àšk béfòré ïñštàllïñg, lïštïñg thé éxàçt pàçkàgéš àñd thé éñvïròñméñt théÿ gò ïñtò. Déçlïñïñg rùñš thé çéll àñÿwàÿ.···!!]", + "configuration.python.interpreterPath.description": "[!!Pàth tò thé Pÿthòñ ïñtérprétér ùšéd bÿ Pÿthòñ çéllš. Ïf émptÿ, Véršò dïšçòvérš òñé fròm thé àçtïvé vïrtùàl éñvïròñméñt, thé wòrkšpàçé, àñd wéll-kñòwñ ïñštàll lòçàtïòñš. Çhàñgéš àpplÿ òñ ñéxt ñòtébòòk òpéñ òr kérñél réštàrt.···!!]", + "configuration.python.useUv.description": "[!!Ùšé thé ùv tòòl tò ïñštàll pàçkàgéš àñd çréàté Pÿthòñ éñvïròñméñtš whéñ ït ïš òñ PÀTH. Whéñ òff, pïp àñd thé štàñdàrd lïbràrÿ véñv mòdùlé àré ùšéd. Çhàñgéš àpplÿ òñ ñéxt ñòtébòòk òpéñ òr kérñél réštàrt.···!!]", + "configuration.showOpenInVersoMenu.description": "[!!Šhòw thé 'Òpéñ àš Véršò Ñòtébòòk' éñtrÿ ïñ thé Éxplòrér çòñtéxt méñù fòr Màrkdòwñ (.md) fïléš. Dïšàblïñg thïš hïdéš thé méñù éñtrÿ òñlÿ; .md fïléš çàñ štïll bé òpéñéd ïñ Véršò thròùgh thé édïtòr'š 'Réòpéñ Édïtòr Wïth...' pïçkér.···!!]", + "extension.description": "[!!Pòlÿglòt .ÑÉT ñòtébòòkš wïth Ç#, F#, Pÿthòñ, JàvàŠçrïpt, TÿpéŠçrïpt, PòwérŠhéll, ŠQL, àñd HTTP çéllš. Òñé šhàréd vàrïàblé štòré àçròšš làñgùàgéš, ÏñtéllïŠéñšé, dàšhbòàrdš, Jùpÿtér ïmpòrt, àñd GïtHùb Çòpïlòt ïñtégràtïòñ.···!!]", + "tool.addCell.displayName": "[!!Àdd Çéll···!!]", + "tool.addParameter.displayName": "[!!Àdd Pàràmétér···!!]", + "tool.changeCellLanguage.displayName": "[!!Çhàñgé Çéll Làñgùàgé···!!]", + "tool.changeCellType.displayName": "[!!Çhàñgé Çéll Tÿpé···!!]", + "tool.getCellProperties.displayName": "[!!Gét Çéll Pròpértïéš···!!]", + "tool.getLanguages.displayName": "[!!Gét Làñgùàgéš···!!]", + "tool.inspectVariable.displayName": "[!!Ïñšpéçt Vàrïàblé···!!]", + "tool.listCells.displayName": "[!!Lïšt Çéllš···!!]", + "tool.listLayouts.displayName": "[!!Lïšt Làÿòùtš···!!]", + "tool.listParameters.displayName": "[!!Lïšt Pàràmétérš···!!]", + "tool.listVariables.displayName": "[!!Lïšt Vàrïàbléš···!!]", + "tool.moveCell.displayName": "[!!Mòvé Çéll···!!]", + "tool.removeCell.displayName": "[!!Rémòvé Çéll···!!]", + "tool.removeParameter.displayName": "[!!Rémòvé Pàràmétér···!!]", + "tool.runAll.displayName": "[!!Rùñ Àll Çéllš···!!]", + "tool.runCell.displayName": "[!!Rùñ Çéll···!!]", + "tool.switchLayout.displayName": "[!!Šwïtçh Làÿòùt···!!]", + "tool.updateCell.displayName": "[!!Ùpdàté Çéll···!!]", + "tool.updateCellProperty.displayName": "[!!Ùpdàté Çéll Pròpértÿ···!!]", + "tool.updateParameter.displayName": "[!!Ùpdàté Pàràmétér···!!]" +} diff --git a/vscode/package.nls.zh-cn.json b/vscode/package.nls.zh-cn.json new file mode 100644 index 00000000..d2c63efc --- /dev/null +++ b/vscode/package.nls.zh-cn.json @@ -0,0 +1,44 @@ +{ + "chat.cells.description": "列出活动笔记本中的所有单元格", + "chat.participant.description": "创建、编辑、运行和探索 Verso 笔记本单元格", + "chat.props.description": "显示某个单元格的属性", + "chat.run.description": "运行活动笔记本中的所有单元格", + "chat.vars.description": "显示作用域内的变量", + "command.compareWithBaseline.title": "比较笔记本与...", + "command.newNotebook.title": "新建笔记本", + "command.openInVerso.title": "作为 Verso Notebook 打开", + "configuration.dotnetPath.description": "用于运行笔记本的 'dotnet' 可执行文件的路径。留空时,Verso 会重用已安装的 .NET 运行时(在可用时通过 .NET Install Tool 定位它),并回退到 PATH 上的 'dotnet'。", + "configuration.extensionsPath.description": "打开笔记本时要加载的第三方 Verso 扩展程序集的目录。每一项为一个目录路径。更改将在下次打开笔记本或重启内核时生效。", + "configuration.hostPath.description": "Verso.Host.dll 的路径。留空时使用捆绑的宿主。", + "configuration.language.auto": "自动检测", + "configuration.language.description": "笔记本界面和内核消息所用的语言。自动检测会跟随 VS Code 的显示语言。菜单项、命令名称和设置说明始终跟随 VS Code 的显示语言,不受此设置影响。更改将在下次打开笔记本时生效。", + "configuration.preserveOriginalFormat.description": "打开 .ipynb 文件时,将更改保存回 .ipynb,而不是转换为 .verso。单元格输出会保留。默认关闭,保持现有的保存时转换行为。Markdown (.md) 笔记本无论此设置如何都会保存回 .md。", + "configuration.python.autoInstall.auto": "无需询问即安装已知的包。对于 Verso 只能猜测的包名,会报告而不安装。", + "configuration.python.autoInstall.description": "当 Python 单元格导入环境中没有的包时的处理方式。更改将在下次打开笔记本或重启内核时生效。", + "configuration.python.autoInstall.off": "从不扫描单元格的导入,也从不安装。", + "configuration.python.autoInstall.prompt": "安装前询问,列出确切的包及其安装到的环境。拒绝后仍会运行该单元格。", + "configuration.python.interpreterPath.description": "Python 单元格使用的 Python 解释器的路径。留空时,Verso 会从活动的虚拟环境、工作区和常见安装位置中发现一个。更改将在下次打开笔记本或重启内核时生效。", + "configuration.python.useUv.description": "当 uv 工具位于 PATH 上时,使用它来安装包和创建 Python 环境。关闭时使用 pip 和标准库的 venv 模块。更改将在下次打开笔记本或重启内核时生效。", + "configuration.showOpenInVersoMenu.description": "在资源管理器上下文菜单中为 Markdown (.md) 文件显示“作为 Verso Notebook 打开”项。禁用此项只会隐藏该菜单项;仍可通过编辑器的“重新打开文件方式...”选取器在 Verso 中打开 .md 文件。", + "extension.description": "支持 C#、F#、Python、JavaScript、TypeScript、PowerShell、SQL 和 HTTP 单元格的多语言 .NET 笔记本。跨语言共享一个变量存储,具备 IntelliSense、仪表板、Jupyter 导入和 GitHub Copilot 集成。", + "tool.addCell.displayName": "添加单元格", + "tool.addParameter.displayName": "添加参数", + "tool.changeCellLanguage.displayName": "更改单元格语言", + "tool.changeCellType.displayName": "更改单元格类型", + "tool.getCellProperties.displayName": "获取单元格属性", + "tool.getLanguages.displayName": "获取语言", + "tool.inspectVariable.displayName": "检查变量", + "tool.listCells.displayName": "列出单元格", + "tool.listLayouts.displayName": "列出布局", + "tool.listParameters.displayName": "列出参数", + "tool.listVariables.displayName": "列出变量", + "tool.moveCell.displayName": "移动单元格", + "tool.removeCell.displayName": "移除单元格", + "tool.removeParameter.displayName": "移除参数", + "tool.runAll.displayName": "运行所有单元格", + "tool.runCell.displayName": "运行单元格", + "tool.switchLayout.displayName": "切换布局", + "tool.updateCell.displayName": "更新单元格", + "tool.updateCellProperty.displayName": "更新单元格属性", + "tool.updateParameter.displayName": "更新参数" +} diff --git a/vscode/src/blazor/blazorBridge.ts b/vscode/src/blazor/blazorBridge.ts index 2df4d619..8684d092 100644 --- a/vscode/src/blazor/blazorBridge.ts +++ b/vscode/src/blazor/blazorBridge.ts @@ -147,7 +147,10 @@ export class BlazorBridge implements vscode.Disposable { this.handleFileDownload(params).catch((err) => { log.error(`file/download error: ${err instanceof Error ? err.message : String(err)}`); vscode.window.showErrorMessage( - `Export failed: ${err instanceof Error ? err.message : String(err)}` + vscode.l10n.t( + "Export failed: {0}", + err instanceof Error ? err.message : String(err) + ) ); }); }); @@ -161,7 +164,10 @@ export class BlazorBridge implements vscode.Disposable { this.handleInputRequest(params).catch((err) => { log.error(`input/request error: ${err instanceof Error ? err.message : String(err)}`); vscode.window.showErrorMessage( - `Input request failed: ${err instanceof Error ? err.message : String(err)}` + vscode.l10n.t( + "Input request failed: {0}", + err instanceof Error ? err.message : String(err) + ) ); }); }); @@ -334,8 +340,16 @@ export class BlazorBridge implements vscode.Disposable { // host reads the file directly from disk. A cancelled dialog returns a null path. const picked = await vscode.window.showOpenDialog({ canSelectMany: false, - openLabel: "Install Extension", - filters: { "Extensions": ["dll", "nupkg"] }, + openLabel: vscode.l10n.t({ + message: "Install Extension", + comment: ["The button that accepts the chosen file, in place of \"Open\"."], + }), + filters: { + [vscode.l10n.t({ + message: "Extensions", + comment: ["Names the kind of file the box will accept."], + })]: ["dll", "nupkg"], + }, }); result = { path: picked?.[0]?.fsPath ?? null }; } else if (method === "diff/sources") { @@ -464,7 +478,9 @@ export class BlazorBridge implements vscode.Disposable { const bytes = Buffer.from(p.data, "base64"); await vscode.workspace.fs.writeFile(uri, bytes); - vscode.window.showInformationMessage(`Exported to ${uri.fsPath}`); + vscode.window.showInformationMessage( + vscode.l10n.t("Exported to {0}", uri.fsPath) + ); return true; } @@ -493,7 +509,14 @@ export class BlazorBridge implements vscode.Disposable { } const value = await vscode.window.showInputBox({ - prompt: p.prompt || "Notebook input", + prompt: + p.prompt || + vscode.l10n.t({ + message: "Notebook input", + comment: [ + "Asked when a running cell wants something typed and did not say what.", + ], + }), password: !!p.isPassword, ignoreFocusOut: true, }); @@ -514,31 +537,50 @@ export class BlazorBridge implements vscode.Disposable { fileName?: string ): Record { const ext = fileName?.split(".").pop()?.toLowerCase(); + // A format's name is the same word in every language, so the kinds below are named + // by dropping it into a translated phrase rather than by translating each pairing. + const named = (format: string) => + vscode.l10n.t({ + message: "{0} Files", + args: [format], + comment: [ + "Names the kind of file a save box will accept. {0} is a format name such as CSV or HTML and is the same word in every language.", + ], + }); + const images = vscode.l10n.t({ + message: "Images", + comment: ["Names the kind of file a save box will accept: pictures."], + }); + const all = vscode.l10n.t({ + message: "All Files", + comment: ["The entry in a save box that accepts any file at all."], + }); switch (contentType) { case "text/csv": - return { "CSV Files": ["csv"], "All Files": ["*"] }; + return { [named("CSV")]: ["csv"], [all]: ["*"] }; case "application/json": - return { "JSON Files": ["json"], "All Files": ["*"] }; + return { [named("JSON")]: ["json"], [all]: ["*"] }; case "text/html": - return { "HTML Files": ["html", "htm"], "All Files": ["*"] }; + return { [named("HTML")]: ["html", "htm"], [all]: ["*"] }; case "text/markdown": - return { "Markdown Files": ["md"], "All Files": ["*"] }; + return { [named("Markdown")]: ["md"], [all]: ["*"] }; case "image/png": - return { Images: ["png"], "All Files": ["*"] }; + return { [images]: ["png"], [all]: ["*"] }; case "image/jpeg": - return { Images: ["jpg", "jpeg"], "All Files": ["*"] }; + return { [images]: ["jpg", "jpeg"], [all]: ["*"] }; case "image/svg+xml": - return { Images: ["svg"], "All Files": ["*"] }; + return { [images]: ["svg"], [all]: ["*"] }; case "image/webp": - return { Images: ["webp"], "All Files": ["*"] }; + return { [images]: ["webp"], [all]: ["*"] }; default: if (ext === "verso") { - return { "Verso Notebooks": ["verso"], "All Files": ["*"] }; + // "Verso Notebooks" is the product's name, so it reads the same everywhere. + return { "Verso Notebooks": ["verso"], [all]: ["*"] }; } if (ext) { - return { Files: [ext], "All Files": ["*"] }; + return { [named(ext.toUpperCase())]: [ext], [all]: ["*"] }; } - return { "All Files": ["*"] }; + return { [all]: ["*"] }; } } @@ -551,7 +593,15 @@ export class BlazorBridge implements vscode.Disposable { this.onDidEdit?.(); } - /** Baseline sources for the Compare menu, with git entries gated on repo membership. */ + /** + * Baseline sources for the Compare menu, with git entries gated on repo membership. + * + * The labels here are for the native quick pick the "Compare Notebook with..." command + * opens, so they follow the editor's display language like the rest of its menus. The + * notebook's own Compare panel names the same four sources from its own resources, + * because that surface follows the notebook interface language instead. Both read from + * the ids below, which are what is actually compared against. + */ listDiffSources(): { sources: Array<{ id: string; @@ -564,33 +614,57 @@ export class BlazorBridge implements vscode.Disposable { const uri = this.documentUri; const hasUri = uri !== undefined; const gitAvailable = hasUri && this.gitProvider.isAvailableFor(uri); - const notInRepo = "The notebook file is not inside a git repository."; + const notInRepo = vscode.l10n.t( + "The notebook file is not inside a git repository." + ); return { sources: [ { id: "lastSaved", - label: "Last Saved", + label: vscode.l10n.t({ + message: "Last Saved", + comment: [ + "One of the things a notebook can be compared against: the copy currently on disk.", + ], + }), kind: "lastSaved", available: hasUri, - description: hasUri ? null : "The notebook has no file on disk yet.", + description: hasUri + ? null + : vscode.l10n.t("The notebook has no file on disk yet."), }, { id: "gitHead", - label: "Git: HEAD", + label: vscode.l10n.t({ + message: "Git: HEAD", + comment: [ + "One of the things a notebook can be compared against. Git and HEAD are version control terms and are not translated.", + ], + }), kind: "git", available: gitAvailable, description: gitAvailable ? null : notInRepo, }, { id: "gitRef", - label: "Git: Compare with Ref...", + label: vscode.l10n.t({ + message: "Git: Compare with Ref...", + comment: [ + "Opens a box for choosing a branch, tag, or commit. Git and Ref are version control terms and are not translated. Keep the three dots, which mean a question follows.", + ], + }), kind: "git", available: gitAvailable, description: gitAvailable ? null : notInRepo, }, { id: "file", - label: "Choose File...", + label: vscode.l10n.t({ + message: "Choose File...", + comment: [ + "Opens a box for picking another notebook to compare against. Keep the three dots, which mean a question follows.", + ], + }), kind: "file", available: true, description: null, @@ -602,47 +676,63 @@ export class BlazorBridge implements vscode.Disposable { /** * Resolves a comparison baseline's content. Pickers (ref quick pick, file dialog) run * natively here; a dismissed picker reports `cancelled` rather than an error. + * + * Names the baseline by `labelKind` and, where the name depends on what was picked, a + * `labelArg`, rather than by a finished sentence. The name is drawn in the notebook's + * Compare panel, which is written in the notebook interface language, not the editor's, + * so the words are chosen there and only the parts nothing can translate, a git ref and + * a file name, travel from here. */ private async resolveDiffBaseline( sourceId: string | undefined ): Promise< - | { content: string; filePath?: string; label: string } + | { + content: string; + filePath?: string; + labelKind: "lastSaved" | "gitHead" | "gitRef" | "file"; + labelArg?: string; + } | { cancelled: true } > { const uri = this.documentUri; switch (sourceId) { case "lastSaved": { if (!uri) { - throw new Error("The notebook has no file on disk yet."); + throw new Error(vscode.l10n.t("The notebook has no file on disk yet.")); } const bytes = await vscode.workspace.fs.readFile(uri); return { content: new TextDecoder().decode(bytes), filePath: uri.fsPath, - label: "Last Saved", + labelKind: "lastSaved", }; } case "gitHead": { if (!uri) { - throw new Error("The notebook has no file on disk yet."); + throw new Error(vscode.l10n.t("The notebook has no file on disk yet.")); } const content = await this.gitProvider.showAtRef(uri, "HEAD"); - return { content, filePath: uri.fsPath, label: "Git: HEAD" }; + return { content, filePath: uri.fsPath, labelKind: "gitHead" }; } case "gitRef": { if (!uri) { - throw new Error("The notebook has no file on disk yet."); + throw new Error(vscode.l10n.t("The notebook has no file on disk yet.")); } const typedItem = { - label: "Type a ref or commit...", + label: vscode.l10n.t({ + message: "Type a ref or commit...", + comment: [ + "The last entry in a list of branches and tags, for naming one that is not listed. Ref and commit are version control terms.", + ], + }), description: "", ref: "__typed__", }; const picked = await vscode.window.showQuickPick( [...this.gitProvider.listRefsForQuickPick(uri), typedItem], - { placeHolder: "Compare notebook with..." } + { placeHolder: vscode.l10n.t("Compare notebook with...") } ); if (!picked) { return { cancelled: true }; @@ -650,7 +740,13 @@ export class BlazorBridge implements vscode.Disposable { const ref = picked.ref === "__typed__" ? await vscode.window.showInputBox({ - prompt: "Git branch, tag, or commit SHA", + prompt: vscode.l10n.t({ + message: "Git branch, tag, or commit SHA", + comment: [ + "Says what may be typed. Every term here is a version control term and stays as written.", + ], + }), + // The default branch name, offered as an example of what to type. placeHolder: "main", }) : picked.ref; @@ -658,14 +754,27 @@ export class BlazorBridge implements vscode.Disposable { return { cancelled: true }; } const content = await this.gitProvider.showAtRef(uri, ref); - return { content, filePath: uri.fsPath, label: `Git: ${ref}` }; + return { + content, + filePath: uri.fsPath, + labelKind: "gitRef", + labelArg: ref, + }; } case "file": { const picked = await vscode.window.showOpenDialog({ canSelectMany: false, - openLabel: "Compare", - filters: { "Notebook Files": ["verso", "ipynb", "dib"] }, + openLabel: vscode.l10n.t({ + message: "Compare", + comment: ["The button that accepts the chosen file, in place of \"Open\"."], + }), + filters: { + [vscode.l10n.t({ + message: "Notebook Files", + comment: ["Names the kind of file the box will accept."], + })]: ["verso", "ipynb", "dib"], + }, }); const file = picked?.[0]; if (!file) { @@ -675,12 +784,15 @@ export class BlazorBridge implements vscode.Disposable { return { content: new TextDecoder().decode(bytes), filePath: file.fsPath, - label: file.path.split("/").pop() ?? "File", + labelKind: "file", + labelArg: file.path.split("/").pop(), }; } default: - throw new Error(`Unknown comparison source '${sourceId}'.`); + throw new Error( + vscode.l10n.t("Unknown comparison source '{0}'.", sourceId ?? "") + ); } } diff --git a/vscode/src/blazor/blazorEditorProvider.ts b/vscode/src/blazor/blazorEditorProvider.ts index afe20569..5e799caf 100644 --- a/vscode/src/blazor/blazorEditorProvider.ts +++ b/vscode/src/blazor/blazorEditorProvider.ts @@ -12,6 +12,7 @@ import { hostRegistry } from "../host/hostRegistry"; import { notebookRegistry } from "../host/notebookRegistry"; import { BlazorBridge } from "./blazorBridge"; import { log } from "../log"; +import { LANGUAGE_SETTING, resolveLanguage } from "../localization"; import { CellAddParams, CellDto, @@ -82,6 +83,19 @@ export class BlazorEditorProvider bridge.postEditorSettings(settings); } } + + // A notebook already open keeps the language it started in. Its interface is a + // WebAssembly app, whose language is fixed when it boots, and its host process + // reads the language once on the command line; changing either means starting + // over, which would throw away whatever had not been saved. Say so, because a + // setting that appears to do nothing is worse than one that waits. + if (e.affectsConfiguration(LANGUAGE_SETTING) && this.bridges.size > 0) { + vscode.window.showInformationMessage( + vscode.l10n.t( + "Verso: the notebooks already open keep their current language. Reopen them to read them in the new one." + ) + ); + } }) ); @@ -136,9 +150,10 @@ export class BlazorEditorProvider fs.mkdirSync(dir, { recursive: true }); } catch (err) { vscode.window.showErrorMessage( - `Verso: Could not create a scratch notebook: ${ - err instanceof Error ? err.message : err - }` + vscode.l10n.t( + "Verso: Could not create a scratch notebook: {0}", + err instanceof Error ? err.message : String(err) + ) ); return; } @@ -155,9 +170,10 @@ export class BlazorEditorProvider fs.writeFileSync(tempPath, ""); } catch (err) { vscode.window.showErrorMessage( - `Verso: Could not create a scratch notebook: ${ - err instanceof Error ? err.message : err - }` + vscode.l10n.t( + "Verso: Could not create a scratch notebook: {0}", + err instanceof Error ? err.message : String(err) + ) ); return; } @@ -199,7 +215,9 @@ export class BlazorEditorProvider // `dotnet ""` and mislabeling it as a runtime problem. if (!this.hostDllPath) { vscode.window.showErrorMessage( - 'Verso: Could not find Verso.Host.dll. Set "verso.hostPath" in settings to the path of your built Verso.Host.dll.' + vscode.l10n.t( + 'Verso: Could not find Verso.Host.dll. Set "verso.hostPath" in settings to the path of your built Verso.Host.dll.' + ) ); return; } @@ -286,9 +304,10 @@ export class BlazorEditorProvider bridge.notify("notebook/opened", { filePath, ...result }); } catch (err) { vscode.window.showErrorMessage( - `Verso: Failed to open notebook: ${ - err instanceof Error ? err.message : err - }` + vscode.l10n.t( + "Verso: Failed to open notebook: {0}", + err instanceof Error ? err.message : String(err) + ) ); } } @@ -411,12 +430,15 @@ export class BlazorEditorProvider ); bridge.endRestart(); bridge.notifyFaulted( - `Restart aborted: the notebook snapshot could not be captured (${ + vscode.l10n.t( + "Restart aborted: the notebook snapshot could not be captured ({0}).", err instanceof Error ? err.message : String(err) - }).` + ) ); vscode.window.showErrorMessage( - "Verso: kernel restart aborted because the notebook snapshot could not be captured. Save and reopen the file." + vscode.l10n.t( + "Verso: kernel restart aborted because the notebook snapshot could not be captured. Save and reopen the file." + ) ); return; } @@ -437,9 +459,10 @@ export class BlazorEditorProvider invalidateDotnetResolution(this.hostDllPath); bridge.endRestart(); bridge.notifyFaulted( - `Kernel restart failed: the host process did not start (${ + vscode.l10n.t( + "Kernel restart failed: the host process did not start ({0}). Close and reopen the notebook.", err instanceof Error ? err.message : String(err) - }). Close and reopen the notebook.` + ) ); // Route through the shared handler so a missing/incompatible .NET runtime // gets the same actionable "Install .NET Runtime" guidance as a fresh open. @@ -463,14 +486,16 @@ export class BlazorEditorProvider ); bridge.endRestart(); bridge.notifyFaulted( - `Kernel restart failed: the notebook did not reopen (${ + vscode.l10n.t( + "Kernel restart failed: the notebook did not reopen ({0}).", err instanceof Error ? err.message : String(err) - }).` + ) ); vscode.window.showErrorMessage( - `Verso: kernel restart failed (notebook did not reopen): ${ + vscode.l10n.t( + "Verso: kernel restart failed (notebook did not reopen): {0}. Close and reopen the notebook.", err instanceof Error ? err.message : String(err) - }. Close and reopen the notebook.` + ) ); return; } @@ -654,6 +679,8 @@ export class BlazorEditorProvider vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? os.homedir(); const target = await vscode.window.showSaveDialog({ defaultUri: vscode.Uri.file(path.join(defaultDir, "Untitled.verso")), + // Names the kind of file the box will accept. "Verso Notebook" is the + // product's name, so the label reads the same in every language. filters: { "Verso Notebook": ["verso"] }, }); if (!target) return; @@ -758,6 +785,12 @@ export class BlazorEditorProvider const wasmRoot = this.getWasmRoot(); const version = this.getCacheBuster(); + // Read once, into the page, because WebAssembly settles its culture while it boots: + // the translations for a language are a separate download that the runtime only knows + // to fetch if it is told the language before the app starts. Assigning a culture after + // that point leaves the app running in English and eventually faults. + const language = resolveLanguage(); + const toUri = (relativePath: string) => webview.asWebviewUri(vscode.Uri.joinPath(wasmRoot, relativePath)).toString() + `?v=${version}`; @@ -1036,16 +1069,29 @@ export class BlazorEditorProvider // The real webview resource base (used by loadBootResource to remap framework fetches). var frameworkBase = '${frameworkBase}/'; var wasmVersion = '${version}'; + // Undefined leaves the app on the browser's own language. + var versoLocale = ${language ? JSON.stringify(language) : "undefined"}; document.addEventListener('DOMContentLoaded', function() { if (typeof Blazor !== 'undefined') { var status = document.getElementById('loading-status'); if (status) status.textContent = 'Starting Blazor runtime...'; Blazor.start({ + applicationCulture: versoLocale, loadBootResource: function(type, name, defaultUri, integrity) { // Remap all framework resource URIs to real webview URIs // since is a synthetic localhost URI. // Append version query param to bust stale caches on extension update. - return frameworkBase + name + '?v=' + wasmVersion; + // + // The path comes from defaultUri rather than from name, because name is + // only ever a file name and some resources sit in a subdirectory. A + // satellite assembly is the case that matters: it lives under its culture, + // at _framework/de/Verso.Blazor.Shared.resources.wasm, and rebuilding the + // URI from name alone asks for it at the root, where it is not. That fetch + // fails, and with it the only reason the app had to load a language. + var marker = '_framework/'; + var at = defaultUri ? defaultUri.lastIndexOf(marker) : -1; + var path = at >= 0 ? defaultUri.substring(at + marker.length) : name; + return frameworkBase + path + '?v=' + wasmVersion; } }).then(function() { if (status) status.textContent = 'Blazor started.'; diff --git a/vscode/src/copilot/participant.ts b/vscode/src/copilot/participant.ts index 9b4b6235..a4a61026 100644 --- a/vscode/src/copilot/participant.ts +++ b/vscode/src/copilot/participant.ts @@ -12,6 +12,9 @@ import { const PARTICIPANT_ID = "verso.copilot.notebook"; +// Read by the model, not by anyone. It stays in English because that is what it was +// written and tested against, and translating it would change how well the model +// follows it without changing anything a reader sees. const BASE_SYSTEM_PROMPT = `You are a Verso notebook assistant integrated into GitHub Copilot Chat. Verso is an interactive notebook environment for .NET, similar in spirit to Jupyter or Polyglot Notebooks. It is NOT built on .NET Interactive: kernel extensions written for Polyglot Notebooks are never loaded, and .NET Interactive idioms do not always transfer. @@ -54,6 +57,50 @@ Guidelines: - Cell numbers are 1-based (cell 1 is the first cell). - After running cells, examine the output and help the user understand results or fix errors.`; +// ── Wording shared by the replies below ───────────────────────────── + +/** + * Said whenever a command needs a notebook and there is not one. Written once so the + * four commands and the main handler cannot drift apart, in wording or in translation. + */ +function noNotebookOpen(): string { + return vscode.l10n.t("No Verso notebook is currently open."); +} + +/** + * A count of cells, in words. + * + * Two entries rather than a "cell(s)" that no other language can copy. Which of them is + * used is decided here because whether a language has a plural form at all, and where the + * boundary falls, is a fact about that language: Japanese and Chinese have one form and + * translate both entries the same way. A language with more forms than two would need a + * real plural rule, and none of the languages Verso ships does. + */ +function cellCount(count: number): string { + return count === 1 + ? vscode.l10n.t({ + message: "{0} cell", + args: [count], + comment: ["Used when {0} is 1. Paired with the entry below."], + }) + : vscode.l10n.t({ + message: "{0} cells", + args: [count], + comment: [ + "Used for every count other than 1. A language with one form for both translates this the same as the entry above.", + ], + }); +} + +/** Said when a cell number is outside the notebook. */ +function cellNotFound(cellNumber: number, total: number): string { + return vscode.l10n.t( + "Cell {0} not found. The notebook has {1}.", + cellNumber, + cellCount(total) + ); +} + interface CellTypeInfo { id: string; displayName: string; @@ -115,7 +162,7 @@ async function handleCellsCommand( ): Promise { const ctx = await resolveNotebook(); if (!ctx) { - stream.markdown("No Verso notebook is currently open."); + stream.markdown(noNotebookOpen()); return {}; } @@ -125,18 +172,24 @@ async function handleCellsCommand( ); if (result.cells.length === 0) { - stream.markdown("The notebook is empty."); + stream.markdown(vscode.l10n.t("The notebook is empty.")); return {}; } stream.markdown( - `**${path.basename(ctx.uri.fsPath)}** - ${result.cells.length} cell(s):\n\n` + `**${path.basename(ctx.uri.fsPath)}** - ${cellCount(result.cells.length)}:\n\n` ); for (let i = 0; i < result.cells.length; i++) { const cell = result.cells[i]; const lang = cell.language ?? cell.type; - stream.markdown(`**Cell ${i + 1}** [${lang}]\n`); + stream.markdown( + `**${vscode.l10n.t({ + message: "Cell {0}", + args: [i + 1], + comment: ["A heading over one cell's code. {0} counts from 1."], + })}** [${lang}]\n` + ); stream.markdown(`\`\`\`${lang}\n${cell.source}\n\`\`\`\n\n`); } @@ -149,11 +202,11 @@ async function handleRunCommand( ): Promise { const ctx = await resolveNotebook(); if (!ctx) { - stream.markdown("No Verso notebook is currently open."); + stream.markdown(noNotebookOpen()); return {}; } - stream.progress("Running all cells..."); + stream.progress(vscode.l10n.t("Running all cells...")); const result = await ctx.host.sendRequest( "execution/runAll", { notebookId: ctx.notebookId } @@ -164,10 +217,24 @@ async function handleRunCommand( ctx.bridge.markDirty(); for (const r of result.results) { - const status = r.status === "completed" ? "completed" : `**${r.status}**`; - stream.markdown(`Cell (${r.elapsedMs}ms): ${status}\n`); + // A status other than "completed" is drawn in bold so a failed run stands out in a + // list of them. The status word itself comes from the host, in the host's language. + const status = r.status === "completed" ? r.status : `**${r.status}**`; + stream.markdown( + `${vscode.l10n.t({ + message: "Cell ({0}ms)", + args: [r.elapsedMs], + comment: ["{0} is a number of milliseconds; ms is the unit and stays as written."], + })}: ${status}\n` + ); if (r.errorMessage) { - stream.markdown(`> Error: ${r.errorMessage}\n`); + stream.markdown( + `> ${vscode.l10n.t({ + message: "Error: {0}", + args: [r.errorMessage], + comment: ["{0} is what the cell reported, in the language the kernel reported it."], + })}\n` + ); } } @@ -180,7 +247,7 @@ async function handleVarsCommand( ): Promise { const ctx = await resolveNotebook(); if (!ctx) { - stream.markdown("No Verso notebook is currently open."); + stream.markdown(noNotebookOpen()); return {}; } @@ -190,11 +257,24 @@ async function handleVarsCommand( ); if (result.variables.length === 0) { - stream.markdown("No variables in scope. Run some cells first."); + stream.markdown( + vscode.l10n.t("No variables in scope. Run some cells first.") + ); return {}; } - stream.markdown("| Name | Type | Value |\n|---|---|---|\n"); + stream.markdown( + `| ${vscode.l10n.t({ + message: "Name", + comment: ["A table heading: what a variable is called."], + })} | ${vscode.l10n.t({ + message: "Type", + comment: ["A table heading: what kind of value a variable holds."], + })} | ${vscode.l10n.t({ + message: "Value", + comment: ["A table heading: what a variable currently holds."], + })} |\n|---|---|---|\n` + ); for (const v of result.variables) { stream.markdown(`| \`${v.name}\` | ${v.typeName} | ${v.valuePreview} |\n`); } @@ -209,14 +289,19 @@ async function handlePropsCommand( ): Promise { const ctx = await resolveNotebook(); if (!ctx) { - stream.markdown("No Verso notebook is currently open."); + stream.markdown(noNotebookOpen()); return {}; } const cellNumber = parseInt(prompt.trim(), 10); if (isNaN(cellNumber) || cellNumber < 1) { stream.markdown( - "Usage: `/props ` (1-based). Example: `/props 2`" + vscode.l10n.t({ + message: "Usage: `/props ` (1-based). Example: `/props 2`", + comment: [ + "Shown when /props was typed without a cell number. Everything in backticks is typed and stays as written.", + ], + }) ); return {}; } @@ -227,9 +312,7 @@ async function handlePropsCommand( ); const cell = cellsResult.cells[cellNumber - 1]; if (!cell) { - stream.markdown( - `Cell ${cellNumber} not found. The notebook has ${cellsResult.cells.length} cell(s).` - ); + stream.markdown(cellNotFound(cellNumber, cellsResult.cells.length)); return {}; } @@ -240,13 +323,15 @@ async function handlePropsCommand( if (result.sections.length === 0) { stream.markdown( - `Cell ${cellNumber} has no configurable properties.` + vscode.l10n.t("Cell {0} has no configurable properties.", cellNumber) ); return {}; } const lang = cell.language ?? cell.type; - stream.markdown(`**Cell ${cellNumber}** [${lang}] properties:\n\n`); + stream.markdown( + vscode.l10n.t("**Cell {0}** [{1}] properties:", cellNumber, lang) + "\n\n" + ); for (const s of result.sections) { stream.markdown(`### ${s.section.title}\n`); @@ -254,15 +339,49 @@ async function handlePropsCommand( stream.markdown(`${s.section.description}\n`); } stream.markdown( - "| Property | Type | Value | Read-only |\n|---|---|---|---|\n" + `| ${vscode.l10n.t({ + message: "Property", + comment: ["A table heading: the name of one setting on a cell."], + })} | ${vscode.l10n.t({ + message: "Type", + comment: ["A table heading: what kind of value a setting takes."], + })} | ${vscode.l10n.t({ + message: "Value", + comment: ["A table heading: what a setting is currently set to."], + })} | ${vscode.l10n.t({ + message: "Read-only", + comment: ["A table heading: whether a setting can be changed."], + })} |\n|---|---|---|---|\n` ); for (const field of s.section.fields) { - const value = field.currentValue ?? "(not set)"; + const value = + field.currentValue ?? + vscode.l10n.t({ + message: "(not set)", + comment: ["Stands in a table cell for a setting that has no value yet."], + }); + const readOnly = field.isReadOnly + ? vscode.l10n.t({ + message: "Yes", + comment: ["A table cell answering whether a setting is read-only."], + }) + : vscode.l10n.t({ + message: "No", + comment: [ + "A table cell answering whether a setting is read-only. The answer to a question, not the word for a number.", + ], + }); stream.markdown( - `| ${field.displayName} | ${field.fieldType} | ${value} | ${field.isReadOnly ? "Yes" : "No"} |\n` + `| ${field.displayName} | ${field.fieldType} | ${value} | ${readOnly} |\n` ); } - stream.markdown(`\n*Provider: \`${s.providerExtensionId}\`*\n\n`); + stream.markdown( + `\n*${vscode.l10n.t({ + message: "Provider: {0}", + args: ["`" + s.providerExtensionId + "`"], + comment: ["Names the extension a group of settings came from. {0} is its id."], + })}*\n\n` + ); } return {}; @@ -293,7 +412,9 @@ const handler: vscode.ChatRequestHandler = async ( // Check if any notebook is open if (hostRegistry.size === 0) { stream.markdown( - "No Verso notebook is currently open. Open a `.verso`, `.ipynb`, `.md`, or `.dib` file first." + vscode.l10n.t( + "No Verso notebook is currently open. Open a `.verso`, `.ipynb`, `.md`, or `.dib` file first." + ) ); return {}; } @@ -301,7 +422,7 @@ const handler: vscode.ChatRequestHandler = async ( // Resolve the active notebook and build an enriched system prompt const ctx = await resolveNotebook(); if (!ctx) { - stream.markdown("No Verso notebook is currently open."); + stream.markdown(noNotebookOpen()); return {}; } @@ -352,7 +473,9 @@ const handler: vscode.ChatRequestHandler = async ( ); } catch (err) { if (err instanceof vscode.LanguageModelError) { - stream.markdown(`Model error: ${err.message}`); + // The message is the model service's own, and arrives in whatever language + // that service answered in. + stream.markdown(vscode.l10n.t("Model error: {0}", err.message)); } return {}; } @@ -402,6 +525,8 @@ const handler: vscode.ChatRequestHandler = async ( ); } catch (err) { const message = err instanceof Error ? err.message : String(err); + // Read by the model, which decides whether to retry or explain, so this one + // stays in English along with the rest of what the tools hand back. toolResults.push( new vscode.LanguageModelToolResultPart(call.callId, [ new vscode.LanguageModelTextPart(`Tool error: ${message}`), diff --git a/vscode/src/copilot/tools.ts b/vscode/src/copilot/tools.ts index f44fb2f6..4dd67752 100644 --- a/vscode/src/copilot/tools.ts +++ b/vscode/src/copilot/tools.ts @@ -53,7 +53,11 @@ export async function resolveNotebook(): Promise { }; }); const picked = await vscode.window.showQuickPick(items, { - placeHolder: "Select a notebook for @verso", + // @verso is how the participant is addressed, so it is typed, not translated. + placeHolder: vscode.l10n.t({ + message: "Select a notebook for @verso", + comment: ["@verso is typed to address the assistant and stays as written."], + }), }); if (!picked) { return undefined; @@ -136,6 +140,15 @@ function resolveCell( return cells[cellNumber - 1]; } +/** + * Wraps a tool's answer for the model. + * + * What goes through here is read by the model, not by anyone, and it stays in English. + * The model reasons over these answers and quotes them back in whatever language the + * conversation is in, so translating them would leave one turn of a conversation written + * in two languages while gaining nothing a reader would ever see. What a reader does see + * while a tool runs, its name and the line describing the call, is translated. + */ function textResult(text: string): vscode.LanguageModelToolResult { return new vscode.LanguageModelToolResult([ new vscode.LanguageModelTextPart(text), @@ -188,8 +201,25 @@ export class AddCellTool ) { const lang = options.input.language; const lines = options.input.source.split("\n").length; + // Two entries rather than a "line(s)" no other language can copy. See the note on + // cellCount in participant.ts for why the choice is made here and not by the + // translation. + const counted = + lines === 1 + ? vscode.l10n.t({ + message: "{0} line", + args: [lines], + comment: ["Used when {0} is 1. Paired with the entry below."], + }) + : vscode.l10n.t({ + message: "{0} lines", + args: [lines], + comment: [ + "Used for every count other than 1. A language with one form for both translates this the same as the entry above.", + ], + }); return { - invocationMessage: `Adding ${lang} cell (${lines} line${lines === 1 ? "" : "s"})`, + invocationMessage: vscode.l10n.t("Adding {0} cell ({1})", lang, counted), }; } @@ -249,7 +279,10 @@ export class UpdateCellTool _token: vscode.CancellationToken ) { return { - invocationMessage: `Updating cell ${options.input.cellNumber}`, + invocationMessage: vscode.l10n.t( + "Updating cell {0}", + options.input.cellNumber + ), }; } @@ -295,11 +328,17 @@ export class RemoveCellTool _token: vscode.CancellationToken ) { return { - invocationMessage: `Removing cell ${options.input.cellNumber}`, + invocationMessage: vscode.l10n.t( + "Removing cell {0}", + options.input.cellNumber + ), confirmationMessages: { - title: "Remove cell", + title: vscode.l10n.t("Remove cell"), message: new vscode.MarkdownString( - `Remove cell **${options.input.cellNumber}** from the notebook?` + vscode.l10n.t( + "Remove cell **{0}** from the notebook?", + options.input.cellNumber + ) ), }, }; @@ -345,7 +384,10 @@ export class RunCellTool _token: vscode.CancellationToken ) { return { - invocationMessage: `Running cell ${options.input.cellNumber}`, + invocationMessage: vscode.l10n.t( + "Running cell {0}", + options.input.cellNumber + ), }; } @@ -401,7 +443,7 @@ export class RunAllTool _token: vscode.CancellationToken ) { return { - invocationMessage: "Running all cells", + invocationMessage: vscode.l10n.t("Running all cells"), }; } @@ -584,7 +626,11 @@ export class AddParameterTool _token: vscode.CancellationToken ) { return { - invocationMessage: `Adding parameter "${options.input.name}" (${options.input.type})`, + invocationMessage: vscode.l10n.t( + 'Adding parameter "{0}" ({1})', + options.input.name, + options.input.type + ), }; } @@ -643,7 +689,10 @@ export class UpdateParameterTool _token: vscode.CancellationToken ) { return { - invocationMessage: `Updating parameter "${options.input.name}"`, + invocationMessage: vscode.l10n.t( + 'Updating parameter "{0}"', + options.input.name + ), }; } @@ -697,11 +746,17 @@ export class RemoveParameterTool _token: vscode.CancellationToken ) { return { - invocationMessage: `Removing parameter "${options.input.name}"`, + invocationMessage: vscode.l10n.t( + 'Removing parameter "{0}"', + options.input.name + ), confirmationMessages: { - title: "Remove parameter", + title: vscode.l10n.t("Remove parameter"), message: new vscode.MarkdownString( - `Remove parameter **${options.input.name}** from the notebook?` + vscode.l10n.t( + "Remove parameter **{0}** from the notebook?", + options.input.name + ) ), }, }; @@ -809,7 +864,11 @@ export class UpdateCellPropertyTool _token: vscode.CancellationToken ) { return { - invocationMessage: `Setting "${options.input.propertyName}" on cell ${options.input.cellNumber}`, + invocationMessage: vscode.l10n.t( + 'Setting "{0}" on cell {1}', + options.input.propertyName, + options.input.cellNumber + ), }; } @@ -898,7 +957,10 @@ export class SwitchLayoutTool _token: vscode.CancellationToken ) { return { - invocationMessage: `Switching layout to "${options.input.layoutId}"`, + invocationMessage: vscode.l10n.t( + 'Switching layout to "{0}"', + options.input.layoutId + ), }; } @@ -972,7 +1034,11 @@ export class MoveCellTool _token: vscode.CancellationToken ) { return { - invocationMessage: `Moving cell ${options.input.cellNumber} to position ${options.input.toPosition}`, + invocationMessage: vscode.l10n.t( + "Moving cell {0} to position {1}", + options.input.cellNumber, + options.input.toPosition + ), }; } @@ -1024,7 +1090,11 @@ export class ChangeCellTypeTool _token: vscode.CancellationToken ) { return { - invocationMessage: `Changing cell ${options.input.cellNumber} to type "${options.input.type}"`, + invocationMessage: vscode.l10n.t( + 'Changing cell {0} to type "{1}"', + options.input.cellNumber, + options.input.type + ), }; } @@ -1082,7 +1152,11 @@ export class ChangeCellLanguageTool _token: vscode.CancellationToken ) { return { - invocationMessage: `Changing cell ${options.input.cellNumber} language to "${options.input.language}"`, + invocationMessage: vscode.l10n.t( + 'Changing cell {0} language to "{1}"', + options.input.cellNumber, + options.input.language + ), }; } diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 1c24fb8c..c4393220 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -23,9 +23,13 @@ export async function activate( if (hostDllPath) { log.info(`Resolved Verso.Host.dll: ${hostDllPath}`); } else { + // The log line stays in English: it is read alongside stack traces and is what + // somebody searches for when they report a problem. log.error('Could not find Verso.Host.dll. Set "verso.hostPath" in settings to the path of your built Verso.Host.dll.'); vscode.window.showErrorMessage( - 'Verso: Could not find Verso.Host.dll. Set "verso.hostPath" in settings to the path of your built Verso.Host.dll.' + vscode.l10n.t( + 'Verso: Could not find Verso.Host.dll. Set "verso.hostPath" in settings to the path of your built Verso.Host.dll.' + ) ); } @@ -80,7 +84,7 @@ export async function activate( const notebook = await resolveNotebook(); if (!notebook) { vscode.window.showInformationMessage( - "Verso: Open a notebook to compare it with a baseline." + vscode.l10n.t("Verso: Open a notebook to compare it with a baseline.") ); return; } @@ -89,18 +93,28 @@ export async function activate( const picked = await vscode.window.showQuickPick( sources.map((s) => ({ label: s.label, - description: s.available ? "" : s.description ?? "unavailable", + description: s.available + ? "" + : s.description ?? vscode.l10n.t({ + message: "unavailable", + comment: [ + "Said of a baseline that cannot be compared against, for a reason nothing here knows.", + ], + }), sourceId: s.id, available: s.available, })), - { placeHolder: "Compare notebook with..." } + { placeHolder: vscode.l10n.t("Compare notebook with...") } ); if (!picked) { return; } if (!picked.available) { vscode.window.showInformationMessage( - `Verso: ${picked.description || "This comparison source is not available."}` + `Verso: ${ + picked.description || + vscode.l10n.t("This comparison source is not available.") + }` ); return; } diff --git a/vscode/src/git/gitBaselineProvider.ts b/vscode/src/git/gitBaselineProvider.ts index 9aa495d6..4ba9b277 100644 --- a/vscode/src/git/gitBaselineProvider.ts +++ b/vscode/src/git/gitBaselineProvider.ts @@ -49,7 +49,9 @@ export class GitBaselineProvider { async showAtRef(uri: vscode.Uri, ref: string): Promise { const repo = this.getRepo(uri); if (!repo) { - throw new Error("This notebook is not inside a git repository."); + throw new Error( + vscode.l10n.t("This notebook is not inside a git repository.") + ); } try { return await repo.show(ref, uri.fsPath); @@ -71,16 +73,51 @@ export class GitBaselineProvider { } const describe = (kind: string) => (r: Ref) => ({ - label: r.name ?? r.commit ?? "(unnamed)", + label: + r.name ?? + r.commit ?? + vscode.l10n.t({ + message: "(unnamed)", + comment: ["Stands in for a branch or tag that has no name to show."], + }), description: `${kind}${r.commit ? ` ${r.commit.substring(0, 8)}` : ""}`, ref: r.name ?? r.commit ?? "", }); const refs = repo.state.refs; return [ - ...refs.filter((r) => r.type === RefType.Head && r.name).map(describe("branch")), - ...refs.filter((r) => r.type === RefType.RemoteHead && r.name).map(describe("remote branch")), - ...refs.filter((r) => r.type === RefType.Tag && r.name).map(describe("tag")), + ...refs + .filter((r) => r.type === RefType.Head && r.name) + .map( + describe( + vscode.l10n.t({ + message: "branch", + comment: [ + "Says what kind of thing is listed, shown beside its name. A line of work in version control.", + ], + }) + ) + ), + ...refs + .filter((r) => r.type === RefType.RemoteHead && r.name) + .map( + describe( + vscode.l10n.t({ + message: "remote branch", + comment: ["A branch that lives on the server rather than on this machine."], + }) + ) + ), + ...refs + .filter((r) => r.type === RefType.Tag && r.name) + .map( + describe( + vscode.l10n.t({ + message: "tag", + comment: ["A name pinned to one point in a project's history."], + }) + ) + ), ].filter((item) => item.ref.length > 0); } @@ -99,17 +136,27 @@ export class GitBaselineProvider { // repository's ENTIRE file listing appended. Never surface that raw message. raw.includes("relative path not found") ) { - return `'${fileName}' is not tracked at '${ref}'. Commit the file first, or pick a different ref.`; + return vscode.l10n.t( + "'{0}' is not tracked at '{1}'. Commit the file first, or pick a different ref.", + fileName, + ref + ); } if (raw.includes("unknown revision") || raw.includes("invalid object name")) { - return `'${ref}' is not a known branch, tag, or commit.`; + return vscode.l10n.t("'{0}' is not a known branch, tag, or commit.", ref); } // Fallback: keep only the first line, capped, so an unexpected git failure stays a - // sentence rather than a wall of output. + // sentence rather than a wall of output. The detail itself is whatever git said, + // which git says in its own language and this cannot translate. const firstLine = raw.split("\n", 1)[0] ?? ""; const detail = firstLine.length > 200 ? `${firstLine.substring(0, 200)}...` : firstLine; - return `git could not read '${fileName}' at '${ref}': ${detail}`; + return vscode.l10n.t( + "git could not read '{0}' at '{1}': {2}", + fileName, + ref, + detail + ); } } diff --git a/vscode/src/host/dotnetRuntime.ts b/vscode/src/host/dotnetRuntime.ts index 6c3c02f4..0ca13eae 100644 --- a/vscode/src/host/dotnetRuntime.ts +++ b/vscode/src/host/dotnetRuntime.ts @@ -156,7 +156,7 @@ export async function showHostStartError( } vscode.window.showErrorMessage( - `Verso: Failed to start host process: ${describeError(err)}` + vscode.l10n.t("Verso: Failed to start host process: {0}", describeError(err)) ); } @@ -171,19 +171,28 @@ async function showDotnetSetupError( lastSetupPromptAt = now; const version = getRequiredRuntimeVersion(hostDllPath); - const message = - `Verso needs the .NET runtime (version ${version} or later) to run ` + - `notebooks, but a compatible installation was not found.`; + const message = vscode.l10n.t( + "Verso needs the .NET runtime (version {0} or later) to run notebooks, but a compatible installation was not found.", + version + ); - const install = "Install .NET Runtime"; - const help = "Setup Help"; + const install = vscode.l10n.t({ + message: "Install .NET Runtime", + comment: ["A button. .NET is a product name and stays as written."], + }); + const help = vscode.l10n.t({ + message: "Setup Help", + comment: ["A button. It opens the page describing how to set Verso up."], + }); const choice = await vscode.window.showErrorMessage(message, install, help); if (choice === install) { const installed = await attemptRuntimeAcquisition(context, hostDllPath); if (installed) { vscode.window.showInformationMessage( - "Verso: the .NET runtime is installed. Reopen the notebook to continue." + vscode.l10n.t( + "Verso: the .NET runtime is installed. Reopen the notebook to continue." + ) ); } else { // On-demand install did not complete (offline, blocked, or declined); @@ -223,7 +232,7 @@ async function attemptRuntimeAcquisition( const acquired = await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, - title: "Verso: installing the .NET runtime...", + title: vscode.l10n.t("Verso: installing the .NET runtime..."), }, () => vscode.commands.executeCommand(INSTALL_TOOL_ACQUIRE, { diff --git a/vscode/src/host/hostProcess.ts b/vscode/src/host/hostProcess.ts index 9970d354..748327ab 100644 --- a/vscode/src/host/hostProcess.ts +++ b/vscode/src/host/hostProcess.ts @@ -7,6 +7,7 @@ import { JsonRpcNotification, } from "./protocol"; import { log } from "../log"; +import { resolveLanguage } from "../localization"; type NotificationHandler = (params: unknown) => void; @@ -69,6 +70,22 @@ function buildHostEnvironment(): NodeJS.ProcessEnv { return env; } +/** + * The command line the host is started with. The language travels here rather than in the + * environment because the environment is inherited by the tools the host launches in turn, + * and a Python interpreter has no business being told what language the notebook chrome is in. + */ +function buildHostArguments(hostDllPath: string): string[] { + const args = [hostDllPath]; + + const language = resolveLanguage(); + if (language) { + args.push("--language", language); + } + + return args; +} + export class HostProcess implements vscode.Disposable { private process: ChildProcess | undefined; private readline: ReadlineInterface | undefined; @@ -133,8 +150,9 @@ export class HostProcess implements vscode.Disposable { 30000 ); - log.info(`Spawning Verso.Host: ${this.dotnetCommand} ${this.hostDllPath}`); - this.process = spawn(this.dotnetCommand, [this.hostDllPath], { + const hostArgs = buildHostArguments(this.hostDllPath); + log.info(`Spawning Verso.Host: ${this.dotnetCommand} ${hostArgs.join(" ")}`); + this.process = spawn(this.dotnetCommand, hostArgs, { stdio: ["pipe", "pipe", "pipe"], env: buildHostEnvironment(), }); @@ -172,8 +190,10 @@ export class HostProcess implements vscode.Disposable { `for the faulting stack; on Linux, see your distro's coredump location.` ); } + // The detail is a signal name or an exit code, which reads the same + // whatever language the sentence around it is in. vscode.window.showWarningMessage( - `Verso host process exited (${detail.toast})` + vscode.l10n.t("Verso host process exited ({0})", detail.toast) ); this.onUnexpectedExit?.(detail.toast); } else { diff --git a/vscode/src/localization.ts b/vscode/src/localization.ts new file mode 100644 index 00000000..be47a958 --- /dev/null +++ b/vscode/src/localization.ts @@ -0,0 +1,77 @@ +import * as vscode from "vscode"; + +/** + * The setting that names the notebook interface language. Exported so a + * configuration-change listener can test for it without repeating the string. + */ +export const LANGUAGE_SETTING = "verso.language"; + +/** + * Language tags Verso ships an interface in, mirroring the .NET side. Kept here rather + * than asked for at runtime because the language has to be decided before anything .NET + * starts: it is passed to the host process on its command line and to the notebook app + * as a boot parameter. + */ +const SHIPPED = ["en", "de", "es", "ja", "zh-Hans"]; + +/** + * VS Code display-language identifiers that are not the language tag .NET expects. + * There are only a handful, and the Chinese pair is the reason this table exists at all: + * `zh-cn` has to become `zh-Hans` because .NET names Chinese by script, and `zh-tw` maps + * to a script Verso does not ship, so it deliberately finds no translation rather than + * being served simplified characters. + */ +const DISPLAY_LANGUAGES: Record = { + "zh-cn": "zh-Hans", + "zh-tw": "zh-Hant", +}; + +/** + * Narrows a language tag onto a shipped language, dropping the region when there is no + * regional translation, so `de-AT` finds German. + * + * @returns The shipped tag, or undefined when the language is not one of them. + */ +function match(tag: string | undefined): string | undefined { + if (!tag) { + return undefined; + } + + const normalized = DISPLAY_LANGUAGES[tag.toLowerCase()] ?? tag; + const candidates = [normalized, normalized.split("-")[0]]; + + for (const candidate of candidates) { + const shipped = SHIPPED.find( + (s) => s.toLowerCase() === candidate.toLowerCase() + ); + if (shipped) { + return shipped; + } + } + + return undefined; +} + +/** + * The language to run the notebook interface and the host process in. + * + * An explicit setting is passed through as written, even when it is not a shipped + * language, so that a hand-set value reaches .NET and is either understood there or + * falls back there. Left on auto, the VS Code display language is used when Verso has + * that language and nothing is returned when it does not, which leaves the .NET side + * free to answer from the environment or the operating system instead. + * + * @returns A language tag, or undefined to let the .NET side decide. + */ +export function resolveLanguage(): string | undefined { + const setting = vscode.workspace + .getConfiguration("verso") + .get("language") + ?.trim(); + + if (setting && setting !== "auto") { + return setting; + } + + return match(vscode.env.language); +} diff --git a/vscode/test/suite/blazorBridge.diff.test.ts b/vscode/test/suite/blazorBridge.diff.test.ts index c7535a3e..a402c14e 100644 --- a/vscode/test/suite/blazorBridge.diff.test.ts +++ b/vscode/test/suite/blazorBridge.diff.test.ts @@ -86,6 +86,25 @@ suite("BlazorBridge diff endpoints", () => { assert.strictEqual(byId.get("file")?.available, true); }); + test("diff source ids are the same in every language", () => { + const { webview } = createFakeWebview(); + const bridge = new BlazorBridge(webview, createFakeHost()); + + // Labels are written in the editor's display language and the notebook writes its + // own from these ids, so an id that moved with the language would break both: the + // notebook could not name the source, and diff/baseline could not resolve it. + assert.deepStrictEqual( + bridge.listDiffSources().sources.map((s) => s.id), + ["lastSaved", "gitHead", "gitRef", "file"] + ); + for (const source of bridge.listDiffSources().sources) { + assert.ok( + source.label.length > 0, + `${source.id} reached the picker with no label` + ); + } + }); + test("diff/sources and diff/baseline never mark the document dirty", async () => { const { webview, emit } = createFakeWebview(); const bridge = new BlazorBridge(webview, createFakeHost()); @@ -171,11 +190,16 @@ suite("BlazorBridge diff endpoints", () => { const baseline = result as { content: string; filePath?: string; - label: string; + labelKind: string; + labelArg?: string; }; assert.strictEqual(baseline.content, payload); - assert.strictEqual(baseline.label, "Last Saved"); assert.strictEqual(baseline.filePath, tempFile.fsPath); + // Says which baseline this is rather than what to call it. The notebook writes + // the name itself, in the notebook interface language, which the reader may have + // set differently from the editor's. + assert.strictEqual(baseline.labelKind, "lastSaved"); + assert.strictEqual(baseline.labelArg, undefined); } finally { await vscode.workspace.fs.delete(tempFile); }