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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
18 changes: 8 additions & 10 deletions .github/workflows/checkout.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,16 @@ jobs:
- name: 🚦 Check code format
id: check-format
timeout-minutes: 1
run: |
find bin -name "*.dart" ! -name "*.*.dart" -print0 | xargs -0 dart format --set-exit-if-changed --line-length 80 -o none bin/
run: dart format --set-exit-if-changed --line-length 80 -o none bin/ lib/ test/

- name: 📈 Check analyzer
id: check-analyzer
timeout-minutes: 1
run: dart analyze --fatal-infos --fatal-warnings bin/
run: dart analyze --fatal-infos --fatal-warnings bin/ lib/ test/

#- name: 🧪 Run unit tests
# id: run-unit-tests
# timeout-minutes: 5
# run: |
# dart test --color --platform=vm --concurrency=12 \
# --timeout=60s --reporter=github --file-reporter=json:reports/tests.json \
# --coverage=coverage -- test/unit_test.dart
- name: 🧪 Run unit tests
id: run-unit-tests
timeout-minutes: 5
run: |
dart test --color --platform=vm --concurrency=12 \
--timeout=60s --reporter=github
72 changes: 71 additions & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,76 @@
"group": "docker"
}
},
{
"label": "sheety:localize",
"detail": "Translate the empty cells of the spreadsheet with OpenAI",
"icon": {
"color": "terminal.ansiMagenta",
"id": "globe"
},
"type": "shell",
"command": [
"flutter pub global run sheety_localization:localize --credentials=credentials.json ",
"--sheet=1QgD5i0U-va3VrljXw8I3o8FxtMJAqrhk3ybbJ9O5mA4 ",
"--token-path=\"openai.key\" ",
"--ignore=\"^help$,^backend,^telegram,^locales$\" ",
"--prompt=\"prompt.txt\" ",
"--model=\"gpt-4o-mini\""
],
"dependsOn": [],
"args": [],
"group": {
"kind": "none",
"isDefault": false
},
"problemMatcher": [],
"options": {
"cwd": "${workspaceFolder}"
},
"isBackground": false,
"presentation": {
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"group": "sheety"
}
},
{
"label": "sheety:generate",
"detail": "Generate ARB and Dart localization files from the spreadsheet",
"icon": {
"color": "terminal.ansiMagenta",
"id": "file-code"
},
"type": "shell",
"command": [
"flutter pub global run sheety_localization:generate --credentials=credentials.json ",
"--sheet=1QgD5i0U-va3VrljXw8I3o8FxtMJAqrhk3ybbJ9O5mA4 ",
"--lib=lib --arb=src/l10n --gen=src/generated --prefix=app --format --no-last-modified ",
"--ignore=\"^help$,^backend,^telegram,^locales$\""
],
"dependsOn": [],
"args": [],
"group": {
"kind": "none",
"isDefault": false
},
"problemMatcher": [],
"options": {
"cwd": "${workspaceFolder}"
},
"isBackground": false,
"presentation": {
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"group": "sheety"
}
},
{
"label": "dart:format",
"detail": "Format all files in the project",
Expand All @@ -97,7 +167,7 @@
},
"type": "shell",
"command": [
"dart format --fix -l 80 lib test"
"dart format --fix -l 80 bin lib test"
],
"dependsOn": [],
"args": [],
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
## 0.5.0

- **BREAKING**: `localize` now only writes to a sheet whose header matches `label | description | meta | en | <locale> ...`. A sheet whose fourth column is not the English source is skipped with a warning — reference tables kept in the same spreadsheet were previously "translated", overwriting their data.
- **FIX**: `localize` wrote nothing back to Google Sheets — the row stream introduced in 0.4.3 never emitted the localized rows.
- **FIX**: `localize` failed every request with `400 Unsupported parameter: 'temperature'` on the default `gpt-5-mini` model. Reasoning models (`gpt-5*`, `o*`) now get `reasoning: {effort: low}` instead of the sampling parameters, and a token budget with headroom for their reasoning tokens.
- **FIX**: Placeholder validation rejected every correctly translated ICU plural, and accepted translations that had flattened the directive away. Placeholders are now parsed by brace depth.
- **FIX**: A reasoning item carrying the model's thinking is no longer mistaken for the answer payload.
- **FIX**: A sheet title containing a space or an apostrophe produced an invalid A1 range, so the row could not be written.
- **FIX**: Two columns whose headers sanitize to the same locale (`pt-BR` and `pt_BR`) left the second one empty forever; the duplicate column is now skipped.
- **CHANGED**: Failed writes to Google Sheets are retried only when retrying can help (429, 5xx, network) — a malformed range or a missing scope no longer costs 60s of sleeping per row.
- **CHANGED**: A single OpenAI request now has a hard timeout (`--timeout`, default 120s), and retries are limited to transient failures; an unusable payload is answered by splitting the batch instead of re-sending the same prompt.
- **CHANGED**: An invalid numeric option (`--workers=abc`, `--batch=99`) is reported instead of being silently replaced by the default.
- **CHANGED**: The pipeline moved to `lib/`, behind interfaces for both the model (`LocalizationClient`) and the spreadsheet (`SheetsGateway`), and is covered by unit tests — including the sheet-write path that used to be reachable only through a live Google account.
- **ADDED**: Per-language fallback: a failed batch of languages is split and each language is retried on its own instead of being re-sent as a whole, so one rare language the model chokes on no longer breaks its neighbours.
- **ADDED**: Translation validation before writing: ICU placeholders and markup tags must survive, no empty values, no leaked markdown fences, no runaway output. A rejected translation is retried alone.
- **ADDED**: Language hints in the prompt and in the JSON schema — English name, native endonym and an explicit disambiguation note for codes models misread (`uk` is Ukrainian, not "United Kingdom").
- **ADDED**: `--timeout` option (default `120s`) — a request the model never finishes is aborted instead of stalling a worker.
- **CHANGED**: Retries are limited to transient failures (network, timeout, 429, 5xx). Unusable payloads are never re-sent with the same prompt.
- **CHANGED**: `max_output_tokens` scales with the number of requested languages, so a large batch is no longer truncated into invalid JSON.
- **CHANGED**: A row that cannot be written to the sheet is skipped instead of aborting the whole run.
- **CHANGED**: The localization pipeline moved to `lib/` and is covered by unit tests.

## 0.4.3

- **CHANGED**: `localize` now writes translated cells to Google Sheets via batch updates per row instead of one request per cell.
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ COPY pubspec.yaml ./
RUN dart pub get --no-example

# Copy source and compile
COPY lib/ lib/
COPY bin/ bin/
RUN dart compile exe bin/generate.dart -o /app/bin/generate && \
dart compile exe bin/localize.dart -o /app/bin/localize
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,11 +412,24 @@ dart pub global run sheety_localization:localize \
Number of languages to translate per single API call. Defaults to `3`. Higher values are faster but may reduce quality for weaker models. Range: 1–20.
- `--workers` (or `-w`):
Number of concurrent API requests. Defaults to `6`, max `14`.
- `--timeout`:
Hard timeout of a single OpenAI request, in seconds. Defaults to `120`, range 10–900. A request that the model never finishes is aborted instead of stalling a worker forever.
- `--ignore` (or `-i`):
Comma-separated list of RegExp patterns to skip sheets whose titles match (e.g. `help,backend-.*,temp-.*`).
Comma-separated list of **regular expressions** — not globs — matched against sheet titles; a sheet whose title matches any of them is skipped (e.g. `^help$,^backend,^temp-`). The match is unanchored, so `backend` also skips `backend-monetization`; anchor with `^`/`$` when you want an exact title. A glob-looking `temp-*` means "temp followed by any number of dashes" and will not do what you expect.
- `--prompt` (or `-p`):
Path to a custom system prompt file for the AI model.

### How Localization Failures Are Handled

> **Only localization sheets are written to.** A sheet is localized only when its header matches `label | description | meta | en | <locale> ...` — that is, when the fourth column is the English source. Reference tables and notes kept in the same spreadsheet have ordinary data in those columns, and translating them would overwrite it, so they are skipped with a warning. `--ignore` remains available for sheets that *do* match the layout but should be left alone anyway.

Language models are unreliable on ambiguous or rare locale codes, so `localize` defends against that:

- **Every locale code is spelled out for the model** — name, native endonym and, for codes that are routinely misread, an explicit warning. `uk` is sent as `uk — Ukrainian (українська) — Ukrainian (Cyrillic script). NOT English and NOT "United Kingdom"`, so it can no longer come back as English.
- **A failed batch is split, not retried.** If a request for a batch of languages fails — timeout, truncated or invalid JSON, garbage output — the batch is *not* re-sent as-is. Each language of that batch is retried on its own, so one problematic rare language cannot take its neighbours down with it.
- **Every translation is validated before it is written**: non-empty, ICU placeholders (`{name}`) and markup tags preserved exactly, no leaked markdown fences, no runaway output. A translation that fails validation is retried alone; only that one language is affected.
- **Partial rows are still saved.** Languages that succeeded are written to the sheet even if one of their neighbours never worked; the failed cell stays empty and is picked up on the next run.

---

## Docker
Expand Down
Loading
Loading