altalandi 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -260
- package/altalandi.mjs +330 -325
- package/auth.mjs +221 -223
- package/batch.mjs +769 -620
- package/client.d.ts +57 -0
- package/client.mjs +52 -13
- package/package.json +1 -1
- package/report.mjs +74 -15
- package/skill/SKILL.md +187 -112
- package/update.mjs +315 -273
package/client.d.ts
CHANGED
|
@@ -87,6 +87,10 @@ export interface RefineResult {
|
|
|
87
87
|
warnings: string[];
|
|
88
88
|
/** true when a test key made the call: no model ran, nothing was charged, output = input. */
|
|
89
89
|
test?: boolean | null;
|
|
90
|
+
/** Characters billed (= billing.characters); absent from engines before character billing. */
|
|
91
|
+
characters?: number;
|
|
92
|
+
/** /v1 only: what the call cost and what is left (characters, chargedIsk, balanceIsk, freeCharsLeftToday, …). */
|
|
93
|
+
billing?: Record<string, unknown> | null;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
export type VerifiedRefineResult = RefineResult & { meaningPreserved: true };
|
|
@@ -113,6 +117,59 @@ export class MeaningVerificationError extends AltalandiError {
|
|
|
113
117
|
/** Refine text and return the full hosted API response. */
|
|
114
118
|
export function refine(text: string, options?: RefineOptions): Promise<RefineResult>;
|
|
115
119
|
|
|
120
|
+
export interface BatchItem {
|
|
121
|
+
/** Your key for the string; defaults to its index, as a string. */
|
|
122
|
+
id?: string;
|
|
123
|
+
/** At most 2,000 characters. */
|
|
124
|
+
text: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface BatchOptions {
|
|
128
|
+
sourceLang?: SourceLang;
|
|
129
|
+
domain?: Domain;
|
|
130
|
+
register?: Register;
|
|
131
|
+
preserveMeaning?: boolean;
|
|
132
|
+
scoreNativeness?: boolean;
|
|
133
|
+
onlyImprove?: boolean;
|
|
134
|
+
glossary?: GlossaryTerm[];
|
|
135
|
+
autoTerminology?: boolean;
|
|
136
|
+
contribute?: boolean;
|
|
137
|
+
apiKey?: string;
|
|
138
|
+
baseUrl?: string;
|
|
139
|
+
timeoutMs?: number;
|
|
140
|
+
fetch?: FetchLike;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface BatchItemResult {
|
|
144
|
+
id: string;
|
|
145
|
+
output: string;
|
|
146
|
+
sourceLang: "en" | "is";
|
|
147
|
+
diffs: RefineDiff[];
|
|
148
|
+
meaningCheck: "passed" | "changed" | "reverted" | "skipped" | "unavailable";
|
|
149
|
+
meaningPreserved: boolean | null;
|
|
150
|
+
warnings: string[];
|
|
151
|
+
/** true: refined in one model call with other strings, then checked on its own. */
|
|
152
|
+
packed: boolean;
|
|
153
|
+
/** Set when this string could not be refined: it came back unchanged and was not billed. */
|
|
154
|
+
error?: string | null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface BatchResult {
|
|
158
|
+
id: string;
|
|
159
|
+
runUrl: string | null;
|
|
160
|
+
items: BatchItemResult[];
|
|
161
|
+
/** Characters billed for the whole batch (= billing.characters); absent from engines before character billing. */
|
|
162
|
+
characters?: number;
|
|
163
|
+
/** @deprecated The word equivalent of `characters` (= billing.units). */
|
|
164
|
+
units: number;
|
|
165
|
+
billing: Record<string, unknown> | null;
|
|
166
|
+
quality: RefineQuality;
|
|
167
|
+
warnings: string[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Refine many short strings in one call (POST /v1/refine/batch). */
|
|
171
|
+
export function refineBatch(items: BatchItem[], options?: BatchOptions): Promise<BatchResult>;
|
|
172
|
+
|
|
116
173
|
/** Refine text and throw unless meaningPreserved is strict true. */
|
|
117
174
|
export function refineVerified(text: string, options?: RefineOptions): Promise<VerifiedRefineResult>;
|
|
118
175
|
|
package/client.mjs
CHANGED
|
@@ -14,6 +14,18 @@ const REQUEST_FIELDS = [
|
|
|
14
14
|
"returnDiffs",
|
|
15
15
|
"contribute",
|
|
16
16
|
];
|
|
17
|
+
// /v1/refine/batch takes fewer options: no writer/scoring tiers, polish or diffs switch.
|
|
18
|
+
const BATCH_REQUEST_FIELDS = [
|
|
19
|
+
"sourceLang",
|
|
20
|
+
"domain",
|
|
21
|
+
"register",
|
|
22
|
+
"preserveMeaning",
|
|
23
|
+
"scoreNativeness",
|
|
24
|
+
"onlyImprove",
|
|
25
|
+
"glossary",
|
|
26
|
+
"autoTerminology",
|
|
27
|
+
"contribute",
|
|
28
|
+
];
|
|
17
29
|
|
|
18
30
|
export class AltalandiError extends Error {
|
|
19
31
|
constructor(message, { code = "ALTALANDI_ERROR", status, retryAfterMs } = {}) {
|
|
@@ -80,16 +92,10 @@ function requestBody(text, options) {
|
|
|
80
92
|
return body;
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
|
|
87
|
-
*/
|
|
88
|
-
export async function refine(text, options = {}) {
|
|
89
|
-
if (typeof text !== "string" || !text.trim()) {
|
|
90
|
-
throw new AltalandiError("Altalandi requires non-empty text.", { code: "INVALID_INPUT" });
|
|
91
|
-
}
|
|
92
|
-
|
|
95
|
+
/** POST a JSON body to `${baseUrl}${route}` with the key, base URL, timeout and
|
|
96
|
+
* error handling every call shares. Returns the payload when `validShape`
|
|
97
|
+
* accepts it; throws an AltalandiError (with status and Retry-After) otherwise. */
|
|
98
|
+
async function post(route, body, options, validShape) {
|
|
93
99
|
const explicitKey = options.apiKey;
|
|
94
100
|
const selectedKey = explicitKey === undefined ? env("ALTALANDI_API_KEY") : explicitKey;
|
|
95
101
|
if (selectedKey !== undefined && typeof selectedKey !== "string") {
|
|
@@ -129,13 +135,13 @@ export async function refine(text, options = {}) {
|
|
|
129
135
|
try {
|
|
130
136
|
let response;
|
|
131
137
|
try {
|
|
132
|
-
response = await fetchImpl(`${baseUrl}
|
|
138
|
+
response = await fetchImpl(`${baseUrl}${route}`, {
|
|
133
139
|
method: "POST",
|
|
134
140
|
headers: {
|
|
135
141
|
"content-type": "application/json",
|
|
136
142
|
authorization: `Bearer ${apiKey}`,
|
|
137
143
|
},
|
|
138
|
-
body: JSON.stringify(
|
|
144
|
+
body: JSON.stringify(body),
|
|
139
145
|
signal: controller.signal,
|
|
140
146
|
});
|
|
141
147
|
} catch {
|
|
@@ -190,7 +196,7 @@ export async function refine(text, options = {}) {
|
|
|
190
196
|
);
|
|
191
197
|
}
|
|
192
198
|
|
|
193
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload) ||
|
|
199
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload) || !validShape(payload)) {
|
|
194
200
|
throw new AltalandiError("Altalandi returned an invalid response shape.", {
|
|
195
201
|
code: "INVALID_RESPONSE",
|
|
196
202
|
status: response.status,
|
|
@@ -203,6 +209,39 @@ export async function refine(text, options = {}) {
|
|
|
203
209
|
}
|
|
204
210
|
}
|
|
205
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Refine English or Icelandic text through the hosted Altalandi API.
|
|
214
|
+
* Client settings (`apiKey`, `baseUrl`, `timeoutMs`, `fetch`) are never sent in
|
|
215
|
+
* the JSON body. The full API result is returned without weakening its verdict.
|
|
216
|
+
*/
|
|
217
|
+
export async function refine(text, options = {}) {
|
|
218
|
+
if (typeof text !== "string" || !text.trim()) {
|
|
219
|
+
throw new AltalandiError("Altalandi requires non-empty text.", { code: "INVALID_INPUT" });
|
|
220
|
+
}
|
|
221
|
+
return post("/v1/refine", requestBody(text, options), options, (p) => typeof p.output === "string");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Refine many short strings in one call (POST /v1/refine/batch): at most 500
|
|
226
|
+
* items of at most 2,000 characters each, 20,000 characters together. Each item
|
|
227
|
+
* comes back with its own output and meaning verdict; an item with `error` came
|
|
228
|
+
* back unchanged and was not billed. Same key, limits and errors as refine().
|
|
229
|
+
*/
|
|
230
|
+
export async function refineBatch(items, options = {}) {
|
|
231
|
+
if (!Array.isArray(items) || !items.length
|
|
232
|
+
|| !items.every((it) => it && typeof it.text === "string" && (it.id === undefined || typeof it.id === "string"))) {
|
|
233
|
+
throw new AltalandiError("Altalandi refineBatch requires a non-empty list of { id?, text } items.", {
|
|
234
|
+
code: "INVALID_INPUT",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const body = { items: items.map((it) => (it.id === undefined ? { text: it.text } : { id: it.id, text: it.text })) };
|
|
238
|
+
for (const field of BATCH_REQUEST_FIELDS) {
|
|
239
|
+
if (options[field] !== undefined) body[field] = options[field];
|
|
240
|
+
}
|
|
241
|
+
return post("/v1/refine/batch", body, options, (p) => Array.isArray(p.items)
|
|
242
|
+
&& p.items.every((it) => it && typeof it === "object" && typeof it.output === "string"));
|
|
243
|
+
}
|
|
244
|
+
|
|
206
245
|
/** Assert that an existing result carries a completed, positive meaning check. */
|
|
207
246
|
export function assertMeaningVerified(result) {
|
|
208
247
|
if (result?.meaningPreserved !== true) {
|
package/package.json
CHANGED
package/report.mjs
CHANGED
|
@@ -88,7 +88,7 @@ export function reportMarkdown(input, data, options = {}) {
|
|
|
88
88
|
`| Málsnið | ${options.register || "neutral"} |`,
|
|
89
89
|
`| Innfæddni | ${q.nativenessScore ?? "n/a"}/100 |`,
|
|
90
90
|
`| Ritill | ${tier.writer || "standard"} / ${tier.scoring || "?"} |`,
|
|
91
|
-
`| Auðkenni | ${data?.id || "?"} |`,
|
|
91
|
+
`| Auðkenni | ${data?.runUrl ? `[${data.id}](${data.runUrl})` : data?.id || "?"} |`,
|
|
92
92
|
"",
|
|
93
93
|
"## Fyrir", "", fence(input), "",
|
|
94
94
|
"## Eftir", "", fence(data?.output), "",
|
|
@@ -154,6 +154,64 @@ export function formatIsk(n) {
|
|
|
154
154
|
|
|
155
155
|
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
156
156
|
|
|
157
|
+
// ── characters (stafir) ─────────────────────────────────────────────────────
|
|
158
|
+
// Billing is in characters. Engines from before the switch send only word
|
|
159
|
+
// units; those are shown as characters too (units × charsPerWord, rounded to
|
|
160
|
+
// tens and marked "um"). The per-100-characters rate is never shown: amounts
|
|
161
|
+
// in krónur and friendly equivalents (an A4 page) only.
|
|
162
|
+
|
|
163
|
+
/** Characters per word, when an engine does not say (`charsPerWord`). */
|
|
164
|
+
export const CHARS_PER_WORD = 6.5;
|
|
165
|
+
/** A normal A4 page (display only). */
|
|
166
|
+
export const CHARS_PER_A4_PAGE = 3200;
|
|
167
|
+
|
|
168
|
+
const singular = (n) => { const k = Math.round(Math.abs(n)); return k % 10 === 1 && k % 100 !== 11; };
|
|
169
|
+
|
|
170
|
+
/** 2690 → "2.690 stafir", 21 → "21 stafur". */
|
|
171
|
+
export function formatChars(n) {
|
|
172
|
+
return `${formatInt(n)} ${singular(n) ? "stafur" : "stafir"}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** One friendly equivalent: "um 7 A4-síður", "um 1 A4-síða", "innan við hálfa A4-síðu". */
|
|
176
|
+
export function charsEquivalent(n) {
|
|
177
|
+
const pages = Math.round(Number(n) / CHARS_PER_A4_PAGE);
|
|
178
|
+
if (pages < 1) return "innan við hálfa A4-síðu";
|
|
179
|
+
return `um ${formatInt(pages)} ${singular(pages) ? "A4-síða" : "A4-síður"}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Word units of an older engine as characters, rounded to tens. */
|
|
183
|
+
export function unitsToChars(units, charsPerWord = CHARS_PER_WORD) {
|
|
184
|
+
return Math.round((Number(units) * charsPerWord) / 10) * 10;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const perWord = (...from) => from.map((o) => num(o?.charsPerWord)).find((v) => v != null && v > 0) ?? CHARS_PER_WORD;
|
|
188
|
+
|
|
189
|
+
/** The first source with a characters field wins; else the first with word
|
|
190
|
+
* units, converted. → { n, approx } or null. */
|
|
191
|
+
function charsOf(sources, charsKey, unitsKey, cpw) {
|
|
192
|
+
for (const s of sources) if (num(s?.[charsKey]) != null) return { n: s[charsKey], approx: false };
|
|
193
|
+
for (const s of sources) if (num(s?.[unitsKey]) != null) return { n: unitsToChars(s[unitsKey], cpw), approx: true };
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Today's free allowance: "ókeypis eftir í dag 9.800 af 15.000 stöfum".
|
|
198
|
+
* `billing` is a call's billing block, `after`/`before` the /v1/me answers
|
|
199
|
+
* after and before it; any may be null. "" when nothing is known. */
|
|
200
|
+
export function freeCharsText(billing, after = null, before = null) {
|
|
201
|
+
const cpw = perWord(billing, after, before);
|
|
202
|
+
const left = charsOf([billing, after], "freeCharsLeftToday", "freeUnitsLeftToday", cpw);
|
|
203
|
+
const daily = charsOf([after, before], "freeDailyChars", "freeDailyUnits", cpw);
|
|
204
|
+
// One "um", before the first converted number that is not 0.
|
|
205
|
+
const um = (c) => (c?.approx && c.n ? "um " : "");
|
|
206
|
+
if (left && daily) {
|
|
207
|
+
const [a, b] = um(left) ? ["um ", ""] : ["", um(daily)];
|
|
208
|
+
return `ókeypis eftir í dag ${a}${formatInt(left.n)} af ${b}${formatInt(daily.n)} ${singular(daily.n) ? "staf" : "stöfum"}`;
|
|
209
|
+
}
|
|
210
|
+
if (left) return `ókeypis eftir í dag ${um(left)}${formatChars(left.n)}`;
|
|
211
|
+
if (daily && daily.n > 0) return `ókeypis ${um(daily)}${formatChars(daily.n)} á dag`;
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
|
|
157
215
|
/**
|
|
158
216
|
* What the call cost and what is left. `data.billing` (sent by newer engines)
|
|
159
217
|
* is exact; otherwise the cost is the balance difference between /v1/me
|
|
@@ -167,27 +225,24 @@ export function costLines(data, before, after) {
|
|
|
167
225
|
if (charged == null && num(before?.balanceIsk) != null && num(after?.balanceIsk) != null) {
|
|
168
226
|
charged = Math.round((before.balanceIsk - after.balanceIsk) * 100) / 100;
|
|
169
227
|
}
|
|
170
|
-
const
|
|
171
|
-
const
|
|
228
|
+
const chars = charsOf([b, data], "characters", "units", perWord(b, data, after, before));
|
|
229
|
+
const charsText = chars ? ` · ${chars.approx ? "um " : ""}${formatChars(chars.n)}` : "";
|
|
172
230
|
const lines = [];
|
|
173
|
-
if (charged == null) lines.push(`- Þetta kall: kostnaður óþekktur${
|
|
174
|
-
else if (charged < 0) lines.push(`- Þetta kall: ekki hægt að reikna (inneignin breyttist á meðan)${
|
|
231
|
+
if (charged == null) lines.push(`- Þetta kall: kostnaður óþekktur${charsText}`);
|
|
232
|
+
else if (charged < 0) lines.push(`- Þetta kall: ekki hægt að reikna (inneignin breyttist á meðan)${charsText}`);
|
|
175
233
|
else if (charged === 0) lines.push(b.paidFrom === "credit"
|
|
176
|
-
? `- Þetta kall: 0 kr. af inneign${
|
|
177
|
-
: `- Þetta kall: 0 kr., ókeypis dagskammtur${
|
|
178
|
-
else lines.push(`- Þetta kall: ${formatIsk(charged)} af inneign${exact ? "" : " (mismunur á inneign fyrir og eftir)"}${
|
|
234
|
+
? `- Þetta kall: 0 kr. af inneign${charsText}`
|
|
235
|
+
: `- Þetta kall: 0 kr., ókeypis dagskammtur${charsText}`);
|
|
236
|
+
else lines.push(`- Þetta kall: ${formatIsk(charged)} af inneign${exact ? "" : " (mismunur á inneign fyrir og eftir)"}${charsText}`);
|
|
179
237
|
|
|
180
238
|
const balance = num(b.balanceIsk) ?? num(after?.balanceIsk);
|
|
181
|
-
const
|
|
182
|
-
const daily = num(after?.freeDailyUnits) ?? num(before?.freeDailyUnits);
|
|
239
|
+
const free = freeCharsText(b, after, before);
|
|
183
240
|
const parts = [];
|
|
184
241
|
if (balance != null) parts.push(`inneign ${formatIsk(balance)}`);
|
|
185
|
-
if (
|
|
186
|
-
else if (left != null) parts.push(`ókeypis eftir í dag ${formatInt(left)} einingar`);
|
|
187
|
-
else if (daily != null && daily > 0) parts.push(`ókeypis ${formatInt(daily)} einingar á dag`);
|
|
242
|
+
if (free) parts.push(free);
|
|
188
243
|
if (parts.length) lines.push(`- Staða: ${parts.join(" · ")}`);
|
|
189
244
|
else lines.push("- Staða: náðist ekki að sækja (`npx altalandi whoami`)");
|
|
190
|
-
if (balance != null && balance < 50) lines.push("- Bæta við inneign: https://altalandi.is/
|
|
245
|
+
if (balance != null && balance < 50) lines.push("- Bæta við inneign: https://altalandi.is/stjornbord/inneign");
|
|
191
246
|
return lines;
|
|
192
247
|
}
|
|
193
248
|
|
|
@@ -279,7 +334,9 @@ export function reviewMarkdown(input, data, { before = null, after = null, maxCh
|
|
|
279
334
|
}
|
|
280
335
|
|
|
281
336
|
out.push("**Kostnaður**", ...costLines(data, before, after));
|
|
282
|
-
|
|
337
|
+
// The run's own page: the text sent, the result, the checks and the review.
|
|
338
|
+
if (data?.runUrl) out.push(`- Verkið: ${data.runUrl}`);
|
|
339
|
+
else if (data?.id) out.push(`- Auðkenni: \`${data.id}\``);
|
|
283
340
|
return out.join("\n") + "\n";
|
|
284
341
|
}
|
|
285
342
|
|
|
@@ -327,6 +384,8 @@ export function batchReportMarkdown(summary) {
|
|
|
327
384
|
`| Kaflar | ${count(t.passages)}: ${count(t.changed)} breyttir, ${count(t.unchangedPassages)} þurftu ekki breytingu, `
|
|
328
385
|
+ `${count(t.kept)} héldu frumtextanum, ${count(t.failed)} mistókust |`,
|
|
329
386
|
`| Stafir sendir | ${count(t.characters)} |`,
|
|
387
|
+
...(Number.isFinite(t.billedCharacters)
|
|
388
|
+
? [`| Stafir rukkaðir | ${t.billedCharactersApprox ? "um " : ""}${count(t.billedCharacters)} |`] : []),
|
|
330
389
|
`| Tegund texta | ${cell(summary?.domain || "general")} |`,
|
|
331
390
|
`| Málsnið | ${cell(summary?.register || "neutral")} |`,
|
|
332
391
|
];
|
package/skill/SKILL.md
CHANGED
|
@@ -1,112 +1,187 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: altalandi
|
|
3
|
-
description: >-
|
|
4
|
-
Turn English or stiff, translated or AI-written Icelandic into natural native
|
|
5
|
-
Icelandic through the Altalandi API, with a meaning check and a diff of every
|
|
6
|
-
change. Use when the user mentions Altalandi, or asks for text in proper,
|
|
7
|
-
natural, correct or native Icelandic ("á góðri íslensku", "á réttri íslensku",
|
|
8
|
-
"láttu þetta hljóma eðlilega á íslensku", "translate this into proper
|
|
9
|
-
Icelandic"), including website copy, UI strings, emails and documents meant for
|
|
10
|
-
Icelandic readers. Not for incidental Icelandic in conversation.
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
# Altalandi: native Icelandic, with a meaning check
|
|
14
|
-
|
|
15
|
-
Altalandi rewrites English or stiff Icelandic so it reads as if a native wrote
|
|
16
|
-
it, checks that the meaning held, and lists every change with its reason. Each
|
|
17
|
-
call is paid from the user's free daily
|
|
18
|
-
|
|
19
|
-
**Follow this procedure exactly, every time.** The user must always see the same
|
|
20
|
-
thing: the verdict, the text before and after, each change with its reason, what
|
|
21
|
-
the call cost and what is left. Do not improvise scripts, parse the JSON
|
|
22
|
-
yourself or summarise the result in your own words.
|
|
23
|
-
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
1
|
+
---
|
|
2
|
+
name: altalandi
|
|
3
|
+
description: >-
|
|
4
|
+
Turn English or stiff, translated or AI-written Icelandic into natural native
|
|
5
|
+
Icelandic through the Altalandi API, with a meaning check and a diff of every
|
|
6
|
+
change. Use when the user mentions Altalandi, or asks for text in proper,
|
|
7
|
+
natural, correct or native Icelandic ("á góðri íslensku", "á réttri íslensku",
|
|
8
|
+
"láttu þetta hljóma eðlilega á íslensku", "translate this into proper
|
|
9
|
+
Icelandic"), including website copy, UI strings, emails and documents meant for
|
|
10
|
+
Icelandic readers. Not for incidental Icelandic in conversation.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Altalandi: native Icelandic, with a meaning check
|
|
14
|
+
|
|
15
|
+
Altalandi rewrites English or stiff Icelandic so it reads as if a native wrote
|
|
16
|
+
it, checks that the meaning held, and lists every change with its reason. Each
|
|
17
|
+
call is paid from the user's free daily characters or their credit.
|
|
18
|
+
|
|
19
|
+
**Follow this procedure exactly, every time.** The user must always see the same
|
|
20
|
+
thing: the verdict, the text before and after, each change with its reason, what
|
|
21
|
+
the call cost and what is left. Do not improvise scripts, parse the JSON
|
|
22
|
+
yourself or summarise the result in your own words.
|
|
23
|
+
|
|
24
|
+
## Which way to call it
|
|
25
|
+
|
|
26
|
+
- **A shell is available (Claude Code, Codex, a terminal):** use the command
|
|
27
|
+
line, below. It waits up to 5 minutes per call and handles long texts.
|
|
28
|
+
- **No shell, only the Altalandi tools** (`refine_icelandic`,
|
|
29
|
+
`proofread_icelandic`, `get_refinement_status`, for example in claude.ai or
|
|
30
|
+
ChatGPT): use the tools, and follow "Long texts through the tools" below. The
|
|
31
|
+
tools cannot wait as long as the command line, so long texts go in parts.
|
|
32
|
+
|
|
33
|
+
How long a call takes: a few seconds for a sentence, 10–30 seconds for a
|
|
34
|
+
paragraph, and one to two minutes for a full page of English. A text that fails
|
|
35
|
+
a check is rewritten once more, which can double that.
|
|
36
|
+
|
|
37
|
+
## The procedure (command line)
|
|
38
|
+
|
|
39
|
+
1. **Save the text** to a UTF-8 temporary file, e.g. `altalandi-in.txt` in the
|
|
40
|
+
system temp folder. Never put the text on the command line.
|
|
41
|
+
2. **Run one command and give it time to finish.** Set the shell tool's timeout
|
|
42
|
+
to 10 minutes (600000 ms) or run it in the background and wait for it. Never
|
|
43
|
+
stop it early and never start a second call for the same text while one is
|
|
44
|
+
running: Altalandi is still working and the second call is charged again.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx -y altalandi@latest --review --file <temp>/altalandi-in.txt --out <temp>/altalandi-out.txt
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Add `--domain marketing` for website and product copy, `--register formal`
|
|
51
|
+
for official text, `--register warm` for friendly customer messages.
|
|
52
|
+
3. **Paste its stdout into your reply exactly as printed**, as the first thing in
|
|
53
|
+
your reply. It is Markdown: a verdict line, **Fyrir**, **Eftir**,
|
|
54
|
+
**Breytingar** (each change and its reason), **Kostnaður** (cost of this call,
|
|
55
|
+
balance and free characters left) and **Verkið**, the run's own page. Do not
|
|
56
|
+
shorten, reorder, translate or reword it. The run page shows the user the
|
|
57
|
+
text sent, the result and the checks, and lets them approve, reject or edit
|
|
58
|
+
the result there; point them to it when they want to review the work.
|
|
59
|
+
4. **Then act on the exit code:**
|
|
60
|
+
|
|
61
|
+
| Exit | What happened | What you do |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| 0 | rewritten and verified, or no change needed | the result is in `altalandi-out.txt`. If the user asked you to update a file, apply it from there; otherwise ask whether to apply it |
|
|
64
|
+
| 3 | the rewrite was refused or the meaning could not be verified | keep the user's original text. Say in one sentence that Altalandi left it unchanged and why (the block says why). Do not apply anything |
|
|
65
|
+
| 4 | out of credit | stop. The block shows the balance and the link to add credit. Do not retry |
|
|
66
|
+
| 2 | not logged in | connect (below), then run step 2 again |
|
|
67
|
+
| 1 | other error (network, too many calls, timeout) | the block says what happened. For "too many calls", wait a minute and retry once. For a timeout, see "If a call times out" |
|
|
68
|
+
|
|
69
|
+
5. **Stop there.** After the block and the action, at most one or two sentences
|
|
70
|
+
of your own.
|
|
71
|
+
|
|
72
|
+
A document of several sections (more than about 3,000 characters) goes better
|
|
73
|
+
through the folder run below: it refines and checks each passage on its own,
|
|
74
|
+
and a passage that fails keeps its original without holding up the rest. Save
|
|
75
|
+
the text as a `.md` file in an empty temporary folder and run the folder steps.
|
|
76
|
+
|
|
77
|
+
## Long texts through the tools
|
|
78
|
+
|
|
79
|
+
1. **Split the text into parts** at paragraph breaks, never inside a sentence:
|
|
80
|
+
at most **1,000 characters** a part for English, **1,500** for Icelandic.
|
|
81
|
+
Keep a heading with the paragraph under it. A list or a table stays in one
|
|
82
|
+
part when it fits.
|
|
83
|
+
2. **Call the tool once per part,** at most 3 at a time, in order. Give every
|
|
84
|
+
part its own new `operationId` (a fresh UUID). Keep a note of which ID
|
|
85
|
+
belongs to which part.
|
|
86
|
+
3. **Join the results in the original order** and give the user the whole text,
|
|
87
|
+
then each part's verdict and changes, as the tool returns them.
|
|
88
|
+
4. **A part that is refused or unverified keeps its original text.** Say which
|
|
89
|
+
part, and why (the tool says why). Never rewrite it yourself.
|
|
90
|
+
5. **Terms must match across parts.** If two parts translate the same term
|
|
91
|
+
differently, point it out and ask which to keep; do not quietly change one.
|
|
92
|
+
|
|
93
|
+
## If a call times out
|
|
94
|
+
|
|
95
|
+
A timeout means the answer did not reach you, not that the work stopped.
|
|
96
|
+
Altalandi keeps working and charges for the call once.
|
|
97
|
+
|
|
98
|
+
With the command line this is rare (it waits 5 minutes). If it happens, do
|
|
99
|
+
not run the text again: tell the user, and that they can see whether it was
|
|
100
|
+
charged at https://altalandi.is/stjornbord/notkun. With the tools:
|
|
101
|
+
|
|
102
|
+
1. **Call `get_refinement_status` with the same `operationId`.** It costs
|
|
103
|
+
nothing.
|
|
104
|
+
2. **Act on the status:**
|
|
105
|
+
|
|
106
|
+
| Status | What you do |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `completed` with the result | use the result as if the call had answered |
|
|
109
|
+
| `completed` without the result | the call finished and was charged, but the text was not kept. Tell the user, and ask before sending that part again with a new `operationId` (it is charged again) |
|
|
110
|
+
| `pending` | still working. Wait 30 seconds and check again, up to 4 times. Do not send the text again meanwhile |
|
|
111
|
+
| `rejected` or `not_found` (`retryable: true`) | nothing was charged. Send the same part again with the **same** `operationId` |
|
|
112
|
+
| `uncertain` | do not retry. Tell the user the call may or may not have finished, and that they can check their usage at https://altalandi.is/stjornbord/notkun |
|
|
113
|
+
|
|
114
|
+
3. **Never get round the protection** by inventing a new `operationId` for a
|
|
115
|
+
text that may still be running. It charges the user twice.
|
|
116
|
+
4. **Next time, send smaller parts** (see above).
|
|
117
|
+
|
|
118
|
+
## Rules
|
|
119
|
+
|
|
120
|
+
- **Never hand-edit the Icelandic and present it as Altalandi's.** If you think
|
|
121
|
+
something should still change, say so separately under a heading such as
|
|
122
|
+
"Mín tillaga (ekki yfirfarin af Altalandi)", or ask before running it again.
|
|
123
|
+
- **One call per coherent text** (a page section, an email). Keep keys,
|
|
124
|
+
placeholders (`{name}`, `%s`), HTML and code out of the text you send. For a
|
|
125
|
+
whole folder, i18n catalog or website, use the folder run below instead.
|
|
126
|
+
- **Many short strings** (UI strings, an i18n catalog, a list of labels or
|
|
127
|
+
messages): **never call Altalandi once per string.** Put them in a JSON file
|
|
128
|
+
(`{"key": "English text", …}`) in an empty temporary folder, or use the
|
|
129
|
+
project's existing catalog folder, and run the folder run below on that folder
|
|
130
|
+
(`--dry-run` first). It sends short strings together in batches, which is
|
|
131
|
+
cheaper and faster, and still checks each string on its own.
|
|
132
|
+
- **Product names and coined words** the meaning check cannot recognise can make
|
|
133
|
+
it refuse a rewrite (exit 3, "ekki tókst að staðfesta"). That is the safe
|
|
134
|
+
result: keep the original and tell the user.
|
|
135
|
+
- **A long text** is shortened in the block; the whole result is in
|
|
136
|
+
`altalandi-out.txt`. Use `--report` when the user wants a full record: it
|
|
137
|
+
writes `./altalandi-reports/altalandi-<time>-<id>.md`, and the block shows
|
|
138
|
+
its path. Reports contain the text, so write them only when asked.
|
|
139
|
+
- **Don't use `--fast`** for text people will read: it skips the meaning check.
|
|
140
|
+
- **Cost is in characters and krónur,** as the block and the estimate show it.
|
|
141
|
+
Never work out or quote a price per 100 characters.
|
|
142
|
+
- If the user only wants spelling fixed and every other word kept, use the
|
|
143
|
+
`proofread_icelandic` tool where it is available. The command line rewrites:
|
|
144
|
+
tell the user it may rephrase, and go through the changes with them.
|
|
145
|
+
- Every call from a signed-in account is kept in that account's history for
|
|
146
|
+
as long as they keep it, so the run page can show it; only the account and its team see it,
|
|
147
|
+
and it can be deleted on the page or switched off in Settings. Separately,
|
|
148
|
+
free use is kept, with personal data scrubbed, to improve the service; paid
|
|
149
|
+
use only with consent. Add `--no-store` when the user asks that their text
|
|
150
|
+
not be kept, or when it is confidential: the call is then kept nowhere, and
|
|
151
|
+
its run page shows only the figures.
|
|
152
|
+
|
|
153
|
+
## A whole folder, i18n catalog or website
|
|
154
|
+
|
|
155
|
+
One command refines every `.md`, `.txt`, `.html` and `.json` file in a folder,
|
|
156
|
+
or a web page, or a site. It keeps code, URLs, placeholders, tags and JSON keys
|
|
157
|
+
byte for byte, never writes to the input, and writes only changes whose meaning
|
|
158
|
+
check passed.
|
|
159
|
+
|
|
160
|
+
1. **Estimate first:** `npx -y altalandi@latest <folder or URL> --dry-run` (add
|
|
161
|
+
`--site` for a whole site). Show the user the estimate it prints: files,
|
|
162
|
+
passages, the characters billed and the price if paid from credit.
|
|
163
|
+
2. **Only when the user agrees,** run the same command with `--yes` in place of
|
|
164
|
+
`--dry-run`, with the shell timeout set to 10 minutes or in the background.
|
|
165
|
+
The result goes to `<folder>-is` unless they name a place (`--out <path>`).
|
|
166
|
+
3. **Show the summary it prints and the report's path.** Exit 0: done. Exit 2:
|
|
167
|
+
done, but some passages kept their original text; the report lists them and
|
|
168
|
+
why. Exit 4: out of credit; the finished files are written, and the same
|
|
169
|
+
command finishes the rest after a top-up.
|
|
170
|
+
|
|
171
|
+
## Connect this computer (once)
|
|
172
|
+
|
|
173
|
+
Only when a call exits with 2 (`Not logged in`):
|
|
174
|
+
|
|
175
|
+
1. Run `npx -y altalandi@latest login` **in the background**. It waits up to 10
|
|
176
|
+
minutes.
|
|
177
|
+
2. As soon as it prints a link (`https://altalandi.is/tengja?kodi=XXXX-XXXX`) and
|
|
178
|
+
a code, show both to the user. Tell them to sign in or create an account
|
|
179
|
+
(a free daily allowance of characters, and 500 kr. of trial credit once they confirm their email) and
|
|
180
|
+
click **Tengja**.
|
|
181
|
+
3. When it prints `✓ Tengt sem <username>`, run step 2 of the procedure again.
|
|
182
|
+
|
|
183
|
+
Never ask the user for an API key, and do not tell them to set
|
|
184
|
+
`ALTALANDI_API_KEY`. `npx -y altalandi@latest whoami` shows the account, the
|
|
185
|
+
balance and the free characters left today at any time.
|
|
186
|
+
|
|
187
|
+
Docs for building the API into an app: https://altalandi.is/agents.md
|