1import * as translate from "@distilled.cloud/aws/translate";
2import * as Effect from "effect/Effect";
3import * as Redacted from "effect/Redacted";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import { createPhysicalName } from "../../PhysicalName.ts";
8import * as Provider from "../../Provider.ts";
9import { Resource } from "../../Resource.ts";
10import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
11import type { Providers } from "../Providers.ts";
12import { readTranslateTags, syncTranslateTags } from "./internal.ts";
13
14export interface TerminologyProps {
15 /**
16 * Name of the terminology — up to 256 characters of letters, digits, and
17 * hyphens (`^([A-Za-z0-9-]_?)+$`). If omitted, a unique name is generated
18 * from the app, stage, and logical ID. Changing the name replaces the
19 * terminology.
20 */
21 terminologyName?: string;
22 /**
23 * Description of the terminology.
24 */
25 description?: string;
26 /**
27 * The terminology file content — term pairs in the given `format`. For
28 * `CSV`, the first row is the language-code header (e.g. `en,es`) and each
29 * subsequent row is a term pair. Updatable in place — `ImportTerminology`
30 * is a true upsert (`MergeStrategy: OVERWRITE`); an overwrite takes up to
31 * 10 minutes to fully propagate to translations.
32 */
33 file: string;
34 /**
35 * Format of the terminology `file`: `CSV`, `TMX`, or `TSV`.
36 */
37 format: "CSV" | "TMX" | "TSV";
38 /**
39 * Directionality of the terminology: `UNI` (source→targets, the default)
40 * or `MULTI` (any listed language is a valid source).
41 * @default "UNI"
42 */
43 directionality?: "UNI" | "MULTI";
44 /**
45 * Id of the customer-managed KMS key used to encrypt the terminology.
46 * If omitted, Translate uses an AWS-owned key.
47 */
48 encryptionKeyId?: string;
49 /**
50 * User-defined tags for the terminology.
51 */
52 tags?: Record<string, string>;
53}
54
55export interface Terminology extends Resource<
56 "AWS.Translate.Terminology",
57 TerminologyProps,
58 {
59 /**
60 * Name of the terminology — pass it as `TerminologyNames` to
61 * `TranslateText`/`TranslateDocument` or batch translation jobs.
62 */
63 terminologyName: string;
64 /**
65 * ARN of the terminology, e.g.
66 * `arn:aws:translate:us-east-1:123456789012:terminology/brand-glossary`.
67 */
68 terminologyArn: string;
69 /** Source language code declared by the terminology file, e.g. `en`. */
70 sourceLanguageCode: string | undefined;
71 /** Target language codes declared by the terminology file. */
72 targetLanguageCodes: string[] | undefined;
73 /** Number of terms imported. */
74 termCount: number | undefined;
75 /** Number of terms skipped during import (malformed rows). */
76 skippedTermCount: number | undefined;
77 /** Size of the imported file in bytes. */
78 sizeBytes: number | undefined;
79 /** Directionality of the terminology (`UNI` or `MULTI`). */
80 directionality: string | undefined;
81 },
82 never,
83 Providers
84> {}
85
86/**
87 * An Amazon Translate custom terminology — a glossary of term pairs (CSV,
88 * TMX, or TSV) that pins how specific words and phrases (brand names,
89 * product names, domain jargon) are translated. Reference it by name from
90 * `TranslateText`, `TranslateDocument`, or batch translation jobs.
91 *
92 * ### Managing Terminologies
93 * **Example:** Import a CSV terminology
94 * ```typescript
95 * const glossary = yield* AWS.Translate.Terminology("BrandGlossary", {
96 * file: ["en,es", "Alchemy,Alquimia"].join("\n"),
97 * format: "CSV",
98 * });
99 * ```
100 *
101 * **Example:** Translate text with the terminology applied
102 * ```typescript
103 * const translateText = yield* AWS.Translate.TranslateText();
104 * const result = yield* translateText({
105 * Text: "Alchemy deploys infrastructure.",
106 * SourceLanguageCode: "en",
107 * TargetLanguageCode: "es",
108 * TerminologyNames: [glossary.terminologyName],
109 * });
110 * ```
111 *
112 * @resource
113 */
114export const Terminology = Resource<Terminology>("AWS.Translate.Terminology");
115
116const toAttributes = (props: translate.TerminologyProperties) => ({
117 terminologyName: props.Name!,
118 terminologyArn: props.Arn!,
119 sourceLanguageCode: props.SourceLanguageCode,
120 targetLanguageCodes: props.TargetLanguageCodes
121 ? [...props.TargetLanguageCodes]
122 : undefined,
123 termCount: props.TermCount,
124 skippedTermCount: props.SkippedTermCount,
125 sizeBytes: props.SizeBytes,
126 directionality: props.Directionality,
127});
128
129export const TerminologyProvider = () =>
130 Provider.effect(
131 Terminology,
132 Effect.gen(function* () {
133 const createName = Effect.fn(function* (
134 id: string,
135 props: TerminologyProps,
136 ) {
137 // Translate names must match ^([A-Za-z0-9-]_?)+$ (≤ 256 chars);
138 // createPhysicalName's hyphenated output satisfies it directly.
139 return (
140 props.terminologyName ??
141 (yield* createPhysicalName({ id, maxLength: 256 }))
142 );
143 });
144
145 const getOne = Effect.fn(function* (name: string) {
146 return yield* translate
147 .getTerminology({ Name: name })
148 .pipe(
149 Effect.catchTag("ResourceNotFoundException", () =>
150 Effect.succeed(undefined),
151 ),
152 );
153 });
154
155 return {
156 stables: ["terminologyName", "terminologyArn"],
157
158 diff: Effect.fn(function* ({ id, olds, news }) {
159 if (!isResolved(news)) return undefined;
160 if ((yield* createName(id, olds)) !== (yield* createName(id, news))) {
161 return { action: "replace" } as const;
162 }
163 }),
164
165 read: Effect.fn(function* ({ id, olds, output }) {
166 const name =
167 output?.terminologyName ??
168 (yield* createName(id, olds ?? { file: "", format: "CSV" }));
169 const found = yield* getOne(name);
170 if (found?.TerminologyProperties === undefined) return undefined;
171 const attrs = toAttributes(found.TerminologyProperties);
172 const tags = yield* readTranslateTags(attrs.terminologyArn);
173 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
174 }),
175
176 reconcile: Effect.fn(function* ({ id, news, olds, output, session }) {
177 const name = output?.terminologyName ?? (yield* createName(id, news));
178 const internalTags = yield* createInternalTags(id);
179 const desiredTags = { ...internalTags, ...news.tags };
180
181 // Observe — cloud state is authoritative.
182 const observed = yield* getOne(name);
183
184 // GetTerminology returns a presigned download location rather than
185 // the file content, so `olds` is the hint for skipping a no-op
186 // import; adoption (olds undefined) always imports to converge.
187 const changed =
188 olds === undefined ||
189 olds.file !== news.file ||
190 olds.format !== news.format ||
191 (olds.directionality ?? "UNI") !== (news.directionality ?? "UNI") ||
192 (olds.description ?? undefined) !==
193 (news.description ?? undefined) ||
194 (olds.encryptionKeyId ?? undefined) !==
195 (news.encryptionKeyId ?? undefined);
196
197 if (observed?.TerminologyProperties === undefined || changed) {
198 const bytes = yield* Effect.sync(() =>
199 new TextEncoder().encode(news.file),
200 );
201 yield* translate.importTerminology({
202 Name: name,
203 MergeStrategy: "OVERWRITE",
204 Description: news.description,
205 TerminologyData: {
206 // The File schema is a SensitiveBlob — the encoder unwraps
207 // the Redacted back to raw bytes on the wire.
208 File: Redacted.make(bytes),
209 Format: news.format,
210 Directionality: news.directionality,
211 },
212 EncryptionKey: news.encryptionKeyId
213 ? { Type: "KMS", Id: news.encryptionKeyId }
214 : undefined,
215 });
216 }
217
218 const final = yield* getOne(name);
219 const attrs = toAttributes(final!.TerminologyProperties!);
220 yield* session.note(name);
221 yield* syncTranslateTags(attrs.terminologyArn, desiredTags);
222 return attrs;
223 }),
224
225 delete: Effect.fn(function* ({ output }) {
226 // Idempotent — the terminology may already be gone.
227 yield* translate
228 .deleteTerminology({ Name: output.terminologyName })
229 .pipe(
230 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
231 );
232 }),
233
234 list: () =>
235 translate.listTerminologies.pages({}).pipe(
236 Stream.runCollect,
237 Effect.map((chunk) =>
238 Array.from(chunk)
239 .flatMap((page) => page.TerminologyPropertiesList ?? [])
240 .filter((props) => props.Name && props.Arn)
241 .map(toAttributes),
242 ),
243 ),
244 };
245 }),
246 );