Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions Docs/TRANSLATING.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Translating the interface

The interface of Signs of AI Writing is translated by whoever wants to translate it. You do not need
to know C#, .NET or Blazor, and you do not need to build anything: a language is **two files' worth
of edits**, both plain JSON.
to know C#, .NET or Blazor. The interface and the saved evidence report are **two independent plain
JSON resources**, because the report's caveats need a stricter completeness check than the app chrome.

If you speak a language this tool doesn't, you can add it. That's the whole idea.

Expand Down Expand Up @@ -32,6 +32,10 @@ on screen; only the text does.

`en.json` is the reference. Every other file is a translation of it.

The document a teacher saves has a second resource under
`src/SignsOfAI.Core/Reporting/report.<language>.json`. Findings in that document still follow the
language of the analysed text; headings and caveats follow the interface language.

---

## Adding a new language
Expand All @@ -52,7 +56,19 @@ change the keys on the left.
"home.stat.words": "Mots",
```

**3. Add one line to `locales.json`:**
**3. Copy `Reporting/report.en.json` to `Reporting/report.fr.json` and translate its values.** Report
translations may be partial, but the fallback notices, caveats, error-rate prose and section headings
must all be present. Those keys form the mandatory core. Until it is complete, reports are written in
English and say on their face that they are — because a report whose limitation cannot be read is
worse than no report, and a report that will not print at all is worse than either: the evidence is
lost and nothing explains why.

Every translated report entry also has a `sourceHash`. It pins that sentence to the exact English
sentence it translated. If English changes later, the tests print the new expected hash and the old
translation falls back visibly until a speaker reviews it. Do not update a hash without rereading the
new English source.

**4. Add one line to `locales.json`:**

```json
{
Expand All @@ -72,17 +88,25 @@ change the keys on the left.
| `endonym` | The language's name **in that language** — `Français`, not `French`. A French speaker looks for `Français`. |
| `credit` | You. Shown when hovering the language switch, so contributors get named. Leave `""` to stay anonymous. |

**4. Open a pull request.** That's it — the switch picks the new language up automatically, no code
**5. Open a pull request.** That's it — the switch picks the new language up automatically, no code
change anywhere.

---

## You don't have to finish

**A partial translation is welcome.** Any key you leave out falls back to English at run time, so
**A partial interface translation is welcome.** Any key you leave out falls back to English at run time, so
half a translation ships as half-translated — not as a page full of blanks. Translate the navigation
and the main page, open the PR, come back for the rest whenever.

A partial report translation is welcome once its mandatory core is complete. Every untranslated
report block carries a notice in the reader's language and the total appears near the top; it never
looks silently complete.

Before the core is complete the report is not withheld — it is written in English, with one notice at
the top naming the language it could not be written in. One honest notice, rather than sixty markers
on a page that fell back entirely.

You can also simply **delete** any key you're unsure about. Deleting is safer than guessing: a
deleted key shows English, while a wrong translation shows something wrong.

Expand Down
395 changes: 253 additions & 142 deletions src/SignsOfAI.Core/Reporting/EvidenceReport.cs

Large diffs are not rendered by default.

433 changes: 433 additions & 0 deletions src/SignsOfAI.Core/Reporting/ReportMessages.cs

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions src/SignsOfAI.Core/Reporting/report.en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"language": "en",
"translators": ["SignsOfAI maintainers"],
"messages": {
"fallback.marker": { "text": "This block has not been translated yet; it is shown in English." },
"fallback.summary": { "text": "This report contains {0} block(s) not yet translated. Each is marked and shown in English." },
"fallback.language": { "text": "This report is not available in {0}, so the whole of it is shown in English. Nothing has been withheld or shortened, but a reader who cannot read English cannot read the part that limits the score, and that part is the point of the page." },
"default.title": { "text": "Writing analysis report" },
"meta.document": { "text": "**Document:** {0}" },
"meta.generated": { "text": "**Generated:** {0} · **Engine:** SignsOfAI {1}" },
"meta.folder": { "text": "**Folder:** {0}" },
"section.analysis": { "text": "What the analysis says" },
"section.checkable": { "text": "Checkable facts" },
"section.characters": { "text": "Characters found in the file" },
"section.citations": { "text": "What the document says about its own sources" },
"section.signals": { "text": "Signals counted" },
"section.observations": { "text": "Found, but at a rate people write at" },
"section.error-rate": { "text": "How often this is wrong" },
"section.unreadable": { "text": "Could not be read" },
"verdict.strong": { "text": "Strong signs of AI writing" },
"verdict.moderate": { "text": "Moderate signs of AI writing" },
"verdict.light": { "text": "Light signs of AI writing" },
"verdict.minimal": { "text": "Reads mostly human" },
"analysis.score.with-verdict": { "text": "**{0}/100 — {1}**" },
"analysis.score.without-verdict": { "text": "**{0}/100**" },
"analysis.no-verdict": { "text": "*Below the threshold this build can support, so no verdict is given. A low score is not evidence that a person wrote this.*" },
"analysis.facts.citation.one": { "text": "**Checkable facts found: {0} source contradiction. These did not move the score.**" },
"analysis.facts.citation.other": { "text": "**Checkable facts found: {0} source contradictions. These did not move the score.**" },
"analysis.facts.artifact.one": { "text": "**Checkable facts found: {0} unusual character. These did not move the score.**" },
"analysis.facts.artifact.other": { "text": "**Checkable facts found: {0} unusual characters. These did not move the score.**" },
"analysis.facts.both.one-one": { "text": "**Checkable facts found: {0} source contradiction, {1} unusual character. These did not move the score.**" },
"analysis.facts.both.one-other": { "text": "**Checkable facts found: {0} source contradiction, {1} unusual characters. These did not move the score.**" },
"analysis.facts.both.other-one": { "text": "**Checkable facts found: {0} source contradictions, {1} unusual character. These did not move the score.**" },
"analysis.facts.both.other-other": { "text": "**Checkable facts found: {0} source contradictions, {1} unusual characters. These did not move the score.**" },
"analysis.counts.one": { "text": "- {0} signal counted" },
"analysis.counts.other": { "text": "- {0} signals counted" },
"analysis.counts-with-observations.one": { "text": "- {0} signal counted, plus {1} found at a rate people write at, which count for nothing" },
"analysis.counts-with-observations.other": { "text": "- {0} signals counted, plus {1} found at a rate people write at, which count for nothing" },
"analysis.language-stats": { "text": "- Analysed as {0} · {1} words · {2} sentences · sentence-length variability {3}" },
"language.en": { "text": "English" },
"language.es": { "text": "Spanish" },
"language.other": { "text": "language code {0}" },
"caveat.uncalibrated": { "text": "> **This build has not been calibrated.** No false-positive rate has been measured for it, so the score above should not be used to support a decision about a person." },
"caveat.aggregate-no-threshold": { "text": "> **No threshold is supported yet.** This build was measured against {0} texts, too few to bound its false-positive rate, so no score on this page should be used to support a decision about a person." },
"caveat.language-unmeasured": { "text": "> **This build has never been measured for {0}.** It has no false-positive rate or supported threshold for writing in this language, and the aggregate result from other languages is not a substitute. No score on this page should be used to support a decision about a person." },
"caveat.language-no-threshold": { "text": "> **No threshold is supported for this language yet.** The corpus holds {0} texts in it — too few to bound how often this build is wrong about writing in it, so no score on this page should be used to support a decision about a person. The best bound these texts support is {1}, and the overall figure is not a substitute for it." },
"caveat.language-measured": { "text": "> **A score is not proof.** On {0} texts in this language, published before generative models existed, this build's false-positive rate at a threshold of {1}/100 was under {2} — the upper end of a 95% interval, not a guarantee, and measured on published articles rather than student work. Below that threshold, treat the score as saying nothing." },
"caveat.aggregate-measured": { "text": "> **A score is not proof.** On {0} texts published before generative models existed, this build's false-positive rate at a threshold of {1}/100 was under {2} — the upper end of a 95% interval, not a guarantee, and measured on published articles rather than student work. Below that threshold, treat the score as saying nothing." },
"checkable.intro": { "text": "These are not judgements about the writing and they did not move the score. Each is either present in the file or it is not." },
"characters.explanation": { "text": "Several of these have ordinary explanations — word processors insert soft hyphens and unusual spaces on their own, and any copy-paste can carry them. Invisible characters and letters borrowed from another alphabet are harder to arrive at by accident, though pasting text can do it. This table says what is in the file, not how it got there." },
"characters.table-header": { "text": "| Character | Codepoint | Line | Column |" },
"common.more-rows": { "text": "… and {0} more." },
"citations.issues-note": { "text": "> None of this needed the internet: the document disagrees with itself. It is a question to ask, not a conclusion — the answer is usually one sentence." },
"citations.no-issues-note": { "text": "> Nothing here is a finding. It describes what could and could not be checked." },
"signals.none": { "text": "None." },
"observations.intro": { "text": "Measured against writing published before generative models existed. Shown because they are real, and counted for nothing because they are ordinary." },
"observations.row.one": { "text": "- {0} — {1} occurrence" },
"observations.row.other": { "text": "- {0} — {1} occurrences" },
"privacy.document": { "text": "*This report was produced on the device that ran the analysis and contains material from the document it describes. It is yours to keep or to send; nothing here was uploaded anywhere.*" },
"folder.summary.one": { "text": "{0} file scanned." },
"folder.summary.other": { "text": "{0} files scanned." },
"folder.summary-unreadable.one": { "text": "{0} file scanned, {1} unreadable." },
"folder.summary-unreadable.other": { "text": "{0} files scanned, {1} unreadable." },
"folder.reading-order": { "text": "> **This is a reading order, not a ranking.** A higher score means look sooner, and nothing more. Nothing on this page establishes that anyone did anything." },
"folder.table-header": { "text": "| File | Score | Signals | Words |" },
"folder.unreadable-row": { "text": "- {0} — {1}" },
"privacy.folder": { "text": "*Produced on the device that scanned the folder. It names your students' files, so treat it as you would the coursework itself; nothing here was uploaded anywhere.*" },
"how.uncalibrated": { "text": "This build ships no calibration, so nothing is known about how often it is wrong. That is itself the most important thing on this page." },
"how.language-unmeasured": { "text": "This build has never been measured on writing in {0}. No language-specific false-positive rate or threshold exists, and the aggregate result from other languages is not a substitute." },
"how.language-no-threshold": { "text": "Measured against **{0} texts in this language**, published before generative models existed, on {2} with engine {3}. That sample is too small to support a threshold; the best upper bound it supports is **{1}**, and the overall figure is not a substitute." },
"how.language-measured": { "text": "Measured against **{0} texts in this language**, published before generative models existed, on {3} with engine {4}. At **{1}/100**, the upper end of the measured 95% false-positive interval was **{2}** — an interval, not a guarantee." },
"how.aggregate-intro": { "text": "Measured against **{0} texts published before generative models existed**, so their authorship rests on their dates rather than on anybody's judgement. Measured on {1} with engine {2}." },
"how.aggregate-threshold": { "text": "At **{0}/100**, {1} of those {2} were flagged — an observed {3}, with a 95% interval of {4} – {5}." },
"how.read-interval": { "text": "Read the interval, not the observed rate. {0} out of {1} is not a false-positive rate you can round down." },
"how.noisy-intro": { "text": "The rules seen most often on that human writing, worst first — if the evidence above leans on one of these, weigh it accordingly:" },
"how.noisy-rule": { "text": "- `{0}` — {1} of human texts" },
"how.limitation": { "text": "What this does **not** tell you: how much machine-written text it catches. That is the other half of the picture and it is deliberately not measured here, because any collection of machine-written text samples whichever models were convenient that month. A tool that flags nothing has a perfect false-positive rate." }
}
}
Loading
Loading