349 | * Web developers:
350 | * Some comments in [W3C TAG design review](https://github.com/w3ctag/design-reviews/issues/948)
351 | * Some comments in [WICG proposal](https://github.com/WICG/proposals/issues/147)
352 |
353 | ## Appendix: converting between language tags and human-readable strings
354 |
355 | This code already works today and is not new to this API proposal. It is likely useful in conjunction with this API, for example when building user interfaces.
356 |
357 | ```js
358 | function languageTagToHumanReadable(languageTag, targetLanguage) {
359 | const displayNames = new Intl.DisplayNames([targetLanguage], { type: "language" });
360 | return displayNames.of(languageTag);
361 | }
362 |
363 | languageTagToHumanReadable("ja", "en"); // "Japanese"
364 | languageTagToHumanReadable("zh", "en"); // "Chinese"
365 | languageTagToHumanReadable("zh-Hant", "en"); // "Traditional Chinese"
366 | languageTagToHumanReadable("zh-TW", "en"); // "Chinese (Taiwan)"
367 |
368 | languageTagToHumanReadable("en", "ja"); // "英語"
369 | ```
370 |
--------------------------------------------------------------------------------
/index.bs:
--------------------------------------------------------------------------------
1 |
2 | Title: Translator and Language Detector APIs
3 | Shortname: translation
4 | Level: None
5 | Status: CG-DRAFT
6 | Group: webml
7 | Repository: webmachinelearning/translation-api
8 | URL: https://webmachinelearning.github.io/translation-api
9 | Editor: Domenic Denicola, Google https://google.com, d@domenic.me, https://domenic.me/
10 | Abstract: The translator and language detector APIs gives web pages the ability to translate text between languages, and detect the language of such text.
11 | Markup Shorthands: markdown yes, css no
12 | Complain About: accidental-2119 yes, missing-example-ids yes
13 | Assume Explicit For: yes
14 | Default Biblio Status: current
15 | Boilerplate: omit conformance
16 | Indent: 2
17 | Die On: warning
18 |
19 |
20 |
21 | urlPrefix: https://tc39.es/ecma402/; spec: ECMA-402
22 | type: dfn
23 | text: Unicode canonicalized locale identifier; url: sec-language-tags
24 | type: abstract-op
25 | text: LookupMatchingLocaleByBestFit; url: sec-lookupmatchinglocalebybestfit
26 | urlPrefix: https://tc39.es/ecma262/; spec: ECMA-262
27 | type: dfn
28 | text: current realm; url: current-realm
29 | urlPrefix: https://whatpr.org/webidl/1465.html; spec: WEBIDL
30 | type: interface
31 | text: QuotaExceededError; url: quotaexceedederror
32 | type: dfn; for: QuotaExceededError
33 | text: requested; url: quotaexceedederror-requested
34 | text: quota; url: quotaexceedederror-quota
35 |
36 |
37 | Introduction
38 |
39 | The translator and language detector APIs expose the ability to translate text between human languages, and detect the language of such text. They are complementary to any built-in browser UI features for these purposes, giving web developers the ability to trigger these operations programmatically and integrate them into their applications. This can be especially useful for operating on user input, or text retrieved from the network.
40 |
41 | These APIs are designed to provide a high-level interface for translation and language detection, abstracting away the complexities of underlying machine learning models and their management. To deal with possible interoperability issues arising from different implementation strategies or language support, the API design guides developers toward checking the availability of languages their applications are dependent on, and including appropriate error-handling.
42 |
43 | Dependencies
44 |
45 | This specification depends on the Infra Standard. [[!INFRA]]
46 |
47 | As with the rest of the web platform, human languages are identified in these APIs by BCP 47 language tags, such as "`ja`", "`en-US`", "`sr-Cyrl`", or "`de-CH-1901-x-phonebk-extended`". The specific algorithms used for validation, canonicalization, and language tag matching are those from the ECMAScript Internationalization API Specification, which in turn defers some of its processing to Unicode Locale Data Markup Language (LDML). [[BCP47]] [[!ECMA-402]] [[UTS35]].
48 |
49 | These APIs are part of a family of APIs expected to be powered by machine learning models, which share common API surface idioms and specification patterns. Currently, the specification text for these shared parts lives in [[WRITING-ASSISTANCE-APIS#supporting]], and the common privacy and security considerations are discussed in [[WRITING-ASSISTANCE-APIS#privacy]] and [[WRITING-ASSISTANCE-APIS#security]]. Implementing these APIs requires implementing that shared infrastructure, and conforming to those privacy and security considerations. But it does not require implementing or exposing the actual writing assistance APIs. [[!WRITING-ASSISTANCE-APIS]]
50 |
51 | The translator API
52 |
53 |
54 | [Exposed=Window, SecureContext]
55 | interface Translator {
56 | static Promise create(TranslatorCreateOptions options);
57 | static Promise availability(TranslatorCreateCoreOptions options);
58 |
59 | Promise translate(
60 | DOMString input,
61 | optional TranslatorTranslateOptions options = {}
62 | );
63 | ReadableStream translateStreaming(
64 | DOMString input,
65 | optional TranslatorTranslateOptions options = {}
66 | );
67 |
68 | readonly attribute DOMString sourceLanguage;
69 | readonly attribute DOMString targetLanguage;
70 |
71 | Promise measureInputUsage(
72 | DOMString input,
73 | optional TranslatorTranslateOptions options = {}
74 | );
75 | readonly attribute unrestricted double inputQuota;
76 | };
77 | Translator includes DestroyableModel;
78 |
79 | dictionary TranslatorCreateCoreOptions {
80 | required DOMString sourceLanguage;
81 | required DOMString targetLanguage;
82 | };
83 |
84 | dictionary TranslatorCreateOptions : TranslatorCreateCoreOptions {
85 | AbortSignal signal;
86 | CreateMonitorCallback monitor;
87 | };
88 |
89 | dictionary TranslatorTranslateOptions {
90 | AbortSignal signal;
91 | };
92 |
93 |
94 | Creation
95 |
96 |
97 | The static create(|options|) method steps are:
98 |
99 | 1. Return the result of [=creating an AI model object=] given |options|, "{{translator}}", [=validate and canonicalize translator options=], [=compute translator options availability=], [=download the translation model=], [=initialize the translation model=], and [=create the translator object=].
100 |
101 |
102 |
103 | To validate and canonicalize translator options given a {{TranslatorCreateCoreOptions}} |options|, perform the following steps. They mutate |options| in place to canonicalize language tags, and throw an exception if any are invalid.
104 |
105 | 1. [=Validate and canonicalize language tags=] given |options| and "{{TranslatorCreateCoreOptions/sourceLanguage}}".
106 |
107 | 1. [=Validate and canonicalize language tags=] given |options| and "{{TranslatorCreateCoreOptions/targetLanguage}}".
108 |
109 |
110 |
111 | To download the translation model, given a {{TranslatorCreateCoreOptions}} |options|:
112 |
113 | 1. [=Assert=]: these steps are running [=in parallel=].
114 |
115 | 1. Initiate the download process for everything the user agent needs to translate text from |options|["{{TranslatorCreateCoreOptions/sourceLanguage}}"] to |options|["{{TranslatorCreateCoreOptions/targetLanguage}}"].
116 |
117 | This could include both a base translation model and specific language arc material, or perhaps material for multiple language arcs if an intermediate language is used.
118 |
119 | 1. If the download process cannot be started for any reason, then return false.
120 |
121 | 1. Return true.
122 |
123 |
124 |
125 | To initialize the translation model, given a {{TranslatorCreateCoreOptions}} |options|:
126 |
127 | 1. [=Assert=]: these steps are running [=in parallel=].
128 |
129 | 1. Perform any necessary initialization operations for the AI model backing the user agent's capabilities for translating from |options|["{{TranslatorCreateCoreOptions/sourceLanguage}}"] to |options|["{{TranslatorCreateCoreOptions/targetLanguage}}"].
130 |
131 | This could include loading the model into memory, or loading any fine-tunings necessary to support the specific options in question.
132 |
133 | 1. If initialization failed for any reason, then return a [=DOMException error information=] whose [=DOMException error information/name=] is "{{OperationError}}" and whose [=DOMException error information/details=] contain appropriate detail.
134 |
135 | 1. Return null.
136 |
137 |
138 |
139 | To
create the translator object, given a [=ECMAScript/realm=] |realm| and a {{TranslatorCreateCoreOptions}} |options|:
140 |
141 | 1. [=Assert=]: these steps are running on |realm|'s [=ECMAScript/surrounding agent=]'s [=agent/event loop=].
142 |
143 | 1. Let |inputQuota| be the amount of input quota that is available to the user agent for future [=translate|translation=] operations. (This value is [=implementation-defined=], and may be +∞ if there are no specific limits beyond, e.g., the user's memory, or the limits of JavaScript strings.)
144 |
145 | 1. Return a new {{Translator}} object, created in |realm|, with
146 |
147 |
148 | : [=Translator/source language=]
149 | :: |options|["{{TranslatorCreateCoreOptions/sourceLanguage}}"]
150 |
151 | : [=Translator/target language=]
152 | :: |options|["{{TranslatorCreateCoreOptions/targetLanguage}}"]
153 |
154 | : [=Translator/input quota=]
155 | :: |inputQuota|
156 |
157 |
158 |
159 | Availability
160 |
161 |
162 | The static availability(|options|) method steps are:
163 |
164 | 1. Return the result of [=computing AI model availability=] given |options|, "{{translator}}", [=validate and canonicalize translator options=], and [=compute translator options availability=].
165 |
166 |
167 |
168 | To
compute translator options availability given a {{TranslatorCreateCoreOptions}} |options|, perform the following steps. They return either an {{Availability}} value or null, and they mutate |options| in place to update language tags to their best-fit matches.
169 |
170 | 1. [=Assert=]: this algorithm is running [=in parallel=].
171 |
172 | 1. Let |availabilities| be the user agent's [=translator language arc availabilities=].
173 |
174 | 1. If |availabilities| is null, then return null.
175 |
176 | 1. [=map/For each=] |languageArc| → |availability| in |availabilities|:
177 |
178 | 1. Let |sourceLanguageBestFit| be [$LookupMatchingLocaleByBestFit$](« |languageArc|'s [=language arc/source language=] », « |options|["{{TranslatorCreateCoreOptions/sourceLanguage}}"] »).
179 |
180 | 1. Let |targetLanguageBestFit| be [$LookupMatchingLocaleByBestFit$](« |languageArc|'s [=language arc/target language=] », « |options|["{{TranslatorCreateCoreOptions/targetLanguage}}"] »).
181 |
182 | 1. If |sourceLanguageBestFit| and |targetLanguageBestFit| are both not undefined, then:
183 |
184 | 1. Set |options|["{{TranslatorCreateCoreOptions/sourceLanguage}}"] to |sourceLanguageBestFit|.\[[locale]].
185 |
186 | 1. Set |options|["{{TranslatorCreateCoreOptions/targetLanguage}}"] to |targetLanguageBestFit|.\[[locale]].
187 |
188 | 1. Return |availability|.
189 |
190 | 1. If (|options|["{{TranslatorCreateCoreOptions/sourceLanguage}}"], |options|["{{TranslatorCreateCoreOptions/targetLanguage}}"]) [=language arc/can be fulfilled by the identity translation=], then return "{{Availability/available}}".
191 |
192 |
Such cases could also return "{{Availability/downloadable}}", "{{Availability/downloading}}", or "{{Availability/available}}" because of the above steps, if the user agent has specific entries in its [=translator language arc availabilities=] for the given language arc. However, the identity translation is always available, so this step ensures that we never return "{{Availability/unavailable}}" for such cases.
193 |
194 |
195 |
One [=language arc=] that [=language arc/can be fulfilled by the identity translation=] is (`"en-US"`, `"en-GB"`). It is conceivable that an implementation might support a specialized model for this translation, which would show up in the [=translator language arc availabilities=].
196 |
197 |
On the other hand, it's pretty unlikely that an implementation has any specialized model for the [=language arc=] ("`en-x-asdf`", "`en-x-xyzw`"). In such a case, this step takes over, and later calls to the [=translate=] algorithm will use the identity translation.
198 |
199 |
Note that when this step takes over, |options|["{{TranslatorCreateCoreOptions/sourceLanguage}}"] and |options|["{{TranslatorCreateCoreOptions/targetLanguage}}"] are not modified, so if this algorithm is being called from {{Translator/create()}}, that means the resulting {{Translator}} object's {{Translator/sourceLanguage}} and {{Translator/targetLanguage}} properties will return the original inputs, and not some canonicalized form.
200 |
201 |
202 | 1. Return "{{Availability/unavailable}}".
203 |
204 |
205 | A language arc is a [=tuple=] of two strings, a source language and a target language. Each item is a [=Unicode canonicalized locale identifier=].
206 |
207 |
208 | The translator language arc availabilities are given by the following steps. They return a [=map=] from [=language arcs=] to {{Availability}} values, or null.
209 |
210 | 1. [=Assert=]: this algorithm is running [=in parallel=].
211 |
212 | 1. If there is some error attempting to determine what language arcs the user agent [=model availability/can support=] translating text between, which the user agent believes to be transient (such that re-querying could stop producing such an error), then return null.
213 |
214 | 1. Return a [=map=] from [=language arcs=] to {{Availability}} values, where each key is a [=language arc=] that the user agent supports translating text between, filled according to the following constraints:
215 |
216 | * If the user agent [=model availability/currently supports=] translating text from the [=language arc/source language=] to the [=language arc/target language=] of the [=language arc=], then the map must contain an [=map/entry=] whose [=map/key=] is that [=language arc=] and whose [=map/value=] is "{{Availability/available}}".
217 |
218 | * If the user agent believes it will be able to [=model availability/support=] translating text from the [=language arc/source language=] to the [=language arc/target language=] of the [=language arc=], but only after finishing a download that is already ongoing, then the map must contain an [=map/entry=] whose [=map/key=] is that [=language arc=] and whose [=map/value=] is "{{Availability/downloading}}".
219 |
220 | * If the user agent believes it will be able to [=model availability/support=] translating text from the [=language arc/source language=] to the [=language arc/target language=] of the [=language arc=], but only after performing a not-currently ongoing download, then the map must contain an [=map/entry=] whose [=map/key=] is that [=language arc=] and whose [=map/value=] is "{{Availability/downloadable}}".
221 |
222 | * The [=map/keys=] must not include any [=language arcs=] that [=language arc/overlap=] with the other [=map/keys=].
223 |
224 |
225 |
226 | Let's suppose that the user agent's [=translator language arc availabilities=] are as follows:
227 |
228 | * ("`en`", "`zh-Hans`") → "{{Availability/available}}"
229 | * ("`en`", "`zh-Hant`") → "{{Availability/downloadable}}"
230 |
231 | The use of [$LookupMatchingLocaleByBestFit$] means that {{Translator/availability()}} will probably give the following answers:
232 |
233 |
234 | function a(sourceLanguage, targetLanguage) {
235 | return ai.translator.availability({ sourceLanguage, targetLanguage }):
236 | }
237 |
238 | await a("en", "zh-Hans") === "available";
239 | await a("en", "zh-Hant") === "downloadable";
240 |
241 | await a("en", "zh") === "available"; // zh will best-fit to zh-Hans
242 |
243 | await a("en", "zh-TW") === "downloadable"; // zh-TW will best-fit to zh-Hant
244 | await a("en", "zh-HK") === "available"; // zh-HK will best-fit to zh-Hans
245 | await a("en", "zh-CN") === "available"; // zh-CN will best-fit to zh-Hans
246 |
247 | await a("en-US", "zh-Hant") === "downloadable"; // en-US will best-fit to en
248 | await a("en-GB", "zh-Hant") === "downloadable"; // en-GB will best-fit to en
249 |
250 | // Even very unexpected subtags will best-fit to en or zh-Hans
251 | await a("en-Braille-x-lolcat", "zh-Hant") === "downloadable";
252 | await a("en", "zh-BR-Kana") === "available";
253 |
254 |
255 |
256 |
257 | A [=language arc=] |arc| overlaps with a [=set=] of [=language arcs=] |otherArcs| if the following steps return true:
258 |
259 | 1. Let |sourceLanguages| be the [=set=] composed of the [=language arc/source languages=] of each [=set/item=] in |otherArcs|.
260 |
261 | 1. If [$LookupMatchingLocaleByBestFit$](|sourceLanguages|, « |arc|'s [=language arc/source language=] ») is not undefined, then return true.
262 |
263 | 1. Let |targetLanguages| be the [=set=] composed of the [=language arc/target languages=] of each [=set/item=] in |otherArcs|.
264 |
265 | 1. If [$LookupMatchingLocaleByBestFit$](|targetLanguages|, « |arc|'s [=language arc/target language=] ») is not undefined, then return true.
266 |
267 | 1. Return false.
268 |
269 |
270 |
271 | The [=language arc=] ("`en`", "`fr`") [=language arc/overlaps=] with « ("`en`", "`fr-CA`") », so the user agent's [=translator language arc availabilities=] cannot contain both of these [=language arcs=] at the same time.
272 |
273 | Instead, a typical user agent will either support only one English-to-French language arc (presumably ("`en`", "`fr`")), or it could support multiple non-overlapping English-to-French language arcs, such as ("`en`", "`fr-FR`"), ("`en`", "`fr-CA`"), and ("`en`", "`fr-CH`").
274 |
275 | In the latter case, if the web developer requested to create a translator using ai.translator.create({ sourceLanguage: "en", targetLanguage: "fr" })
, the [$LookupMatchingLocaleByBestFit$] algorithm would choose one of the three possible language arcs to use (presumably ("`en`", "`fr-FR`")).
276 |
277 |
278 |
279 | A [=language arc=] |arc| can be fulfilled by the identity translation if the following steps return true:
280 |
281 | 1. If [$LookupMatchingLocaleByBestFit$](« |arc|'s [=language arc/source language=] », « |arc|'s [=language arc/target language=] ») is not undefined, then return true.
282 |
283 | 1. If [$LookupMatchingLocaleByBestFit$](« |arc|'s [=language arc/target language=] », « |arc|'s [=language arc/source language=] ») is not undefined, then return true.
284 |
285 | 1. Return false.
286 |
287 |
288 | The {{Translator}} class
289 |
290 | Every {{Translator}} has a source language, a [=string=], set during creation.
291 |
292 | Every {{Translator}} has a target language, a [=string=], set during creation.
293 |
294 | Every {{Translator}} has an input quota, a number, set during creation.
295 |
296 |
297 |
298 | The sourceLanguage getter steps are to return [=this=]'s [=Translator/source language=].
299 |
300 | The targetLanguage getter steps are to return [=this=]'s [=Translator/target language=].
301 |
302 | The inputQuota getter steps are to return [=this=]'s [=Translator/input quota=].
303 |
304 |
305 |
306 |
307 | The translate(|input|, |options|) method steps are:
308 |
309 | 1. Let |operation| be an algorithm step which takes arguments |chunkProduced|, |done|, |error|, and |stopProducing|, and [=translates=] |input| given [=this=]'s [=Translator/source language=], [=this=]'s [=Translator/target language=], [=this=]'s [=Translator/input quota=], |chunkProduced|, |done|, |error|, and |stopProducing|.
310 |
311 | 1. Return the result of [=getting an aggregated AI model result=] given [=this=], |options|, and |operation|.
312 |
313 |
314 |
315 | The translateStreaming(|input|, |options|) method steps are:
316 |
317 | 1. Let |operation| be an algorithm step which takes arguments |chunkProduced|, |done|, |error|, and |stopProducing|, and [=translates=] |input| given [=this=]'s [=Translator/source language=], [=this=]'s [=Translator/target language=], [=this=]'s [=Translator/input quota=], |chunkProduced|, |done|, |error|, and |stopProducing|.
318 |
319 | 1. Return the result of [=getting a streaming AI model result=] given [=this=], |options|, and |operation|.
320 |
321 |
322 |
323 | The measureInputUsage(|input|, |options|) method steps are:
324 |
325 | 1. Let |measureUsage| be an algorithm step which takes argument |stopMeasuring|, and returns the result of [=measuring translator input usage=] given |input|, [=this=]'s [=Translator/source language=], [=this=]'s [=Translator/target language=], and |stopMeasuring|.
326 |
327 | 1. Return the result of [=measuring AI model input usage=] given [=this=], |options|, and |measureUsage|.
328 |
329 |
330 | Translation
331 |
332 | The algorithm
333 |
334 |
335 | To
translate given:
336 |
337 | * a [=string=] |input|,
338 | * a [=Unicode canonicalized locale identifier=] |sourceLanguage|,
339 | * a [=Unicode canonicalized locale identifier=] |targetLanguage|,
340 | * a number |inputQuota|,
341 | * an algorithm |chunkProduced| that takes a string and returns nothing,
342 | * an algorithm |done| that takes no arguments and returns nothing,
343 | * an algorithm |error| that takes [=error information=] and returns nothing, and
344 | * an algorithm |stopProducing| that takes no arguments and returns a boolean,
345 |
346 | perform the following steps:
347 |
348 | 1. [=Assert=]: this algorithm is running [=in parallel=].
349 |
350 | 1. Let |requested| be the result of [=measuring translator input usage=] given |input|, |sourceLanguage|, |targetLanguage|, and |stopProducing|.
351 |
352 | 1. If |requested| is null, then return.
353 |
354 | 1. If |requested| is an [=error information=], then:
355 |
356 | 1. Perform |error| given |requested|.
357 |
358 | 1. Return.
359 |
360 | 1. [=Assert=]: |requested| is a number.
361 |
362 | 1. If |requested| is greater than |inputQuota|, then:
363 |
364 | 1. Let |errorInfo| be a [=quota exceeded error information=] with a [=quota exceeded error information/requested=] of |requested| and a [=quota exceeded error information/quota=] of |inputQuota|.
365 |
366 | 1. Perform |error| given |errorInfo|.
367 |
368 | 1. Return.
369 |
370 |
In reality, we expect that implementations will check the input usage against the quota as part of the same call into the model as the translation itself. The steps are only separated in the specification for ease of understanding.
371 |
372 | 1. In an [=implementation-defined=] manner, subject to the following guidelines, begin the processs of translating |input| from |sourceLanguage| into |targetLanguage|.
373 |
374 | If |input| is the empty string, or otherwise consists of no translatable content (e.g., only contains whitespace, or control characters), then the resulting translation should be |input|. In such cases, |sourceLanguage| and |targetLanguage| should be ignored.
375 |
376 | If (|sourceLanguage|, |targetLanguage|) [=language arc/can be fulfilled by the identity translation=], then the resulting translation should be |input|.
377 |
378 | The translation process must conform to the guidance given in [[WRITING-ASSISTANCE-APIS#privacy]] and [[WRITING-ASSISTANCE-APIS#security]], notably including (but not limited to) [[WRITING-ASSISTANCE-APIS#privacy-user-input]] and [[WRITING-ASSISTANCE-APIS#security-runtime]].
379 |
380 | 1. While true:
381 |
382 | 1. Wait for the next chunk of translated text to be produced, for the translation process to finish, or for the result of calling |stopProducing| to become true.
383 |
384 | 1. If such a chunk is successfully produced:
385 |
386 | 1. Let it be represented as a [=string=] |chunk|.
387 |
388 | 1. Perform |chunkProduced| given |chunk|.
389 |
390 | 1. Otherwise, if the translation process has finished:
391 |
392 | 1. Perform |done|.
393 |
394 | 1. [=iteration/Break=].
395 |
396 | 1. Otherwise, if |stopProducing| returns true, then [=iteration/break=].
397 |
398 | 1. Otherwise, if an error occurred during translation:
399 |
400 | 1. Let the error be represented as a [=DOMException error information=] |errorInfo| according to the guidance in [[#translator-errors]].
401 |
402 | 1. Perform |error| given |errorInfo|.
403 |
404 | 1. [=iteration/Break=].
405 |
406 |
407 | Usage
408 |
409 |
410 | To
measure translator input usage, given:
411 |
412 | * a [=string=] |input|,
413 | * a [=Unicode canonicalized locale identifier=] |sourceLanguage|,
414 | * a [=Unicode canonicalized locale identifier=] |targetLanguage|, and
415 | * an algorithm |stopMeasuring| that takes no arguments and returns a boolean,
416 |
417 | perform the following steps:
418 |
419 | 1. [=Assert=]: this algorithm is running [=in parallel=].
420 |
421 | 1. Let |inputToModel| be the [=implementation-defined=] string that would be sent to the underlying model in order to [=translate=] |input| from |sourceLanguage| to |targetLanguage|.
422 |
423 |
This might be just |input| itself, if |sourceLanguage| and |targetLanguage| were loaded into the model during initialization. Or it might consist of more, e.g. appropriate quota usage for encoding the languages in question, or some sort of wrapper prompt to a language model.
424 |
425 | If during this process |stopMeasuring| starts returning true, then return null.
426 |
427 | If an error occurs during this process, then return an appropriate [=DOMException error information=] according to the guidance in [[#translator-errors]].
428 |
429 | 1. Return the amount of input usage needed to represent |inputToModel| when given to the underlying model. The exact calculation procedure is [=implementation-defined=], subject to the following constraints.
430 |
431 | The returned input usage must be nonnegative and finite. It must be 0, if there are no usage quotas for the translation process (i.e., if the [=Translator/input quota=] is +∞). Otherwise, it must be positive and should be roughly proportional to the [=string/length=] of |inputToModel|.
432 |
433 |
This might be the number of tokens needed to represent |input| in a language model tokenization scheme, or it might be |input|'s [=string/length=]. It could also be some variation of these which also counts the usage of any prefixes or suffixes necessary to give to the model.
434 |
435 | If during this process |stopMeasuring| starts returning true, then instead return null.
436 |
437 | If an error occurs during this process, then instead return an appropriate [=DOMException error information=] according to the guidance in [[#translator-errors]].
438 |
439 |
440 | Errors
441 |
442 | When translation fails, the following possible reasons may be surfaced to the web developer. This table lists the possible {{DOMException}} [=DOMException/names=] and the cases in which an implementation should use them:
443 |
444 |
445 |
446 |
447 | {{DOMException}} [=DOMException/name=]
448 | | Scenarios
449 | |
450 |
451 | "{{NotAllowedError}}"
452 | |
453 | Translation is disabled by user choice or user agent policy.
454 | |
455 | "{{NotReadableError}}"
456 | |
457 | The translation output was filtered by the user agent, e.g., because it was detected to be harmful, inaccurate, or nonsensical.
458 | |
459 | "{{UnknownError}}"
460 | |
461 | All other scenarios, including if the user agent believes it cannot translate and also meet the requirements given in [[WRITING-ASSISTANCE-APIS#privacy]] and [[WRITING-ASSISTANCE-APIS#security]]. Or, if the user agent would prefer not to disclose the failure reason.
462 | |
463 |
464 | This table does not give the complete list of exceptions that can be surfaced by the translator API. It only contains those which can come from certain [=implementation-defined=] steps.
465 |
466 |
Permissions policy integration
467 |
468 | Access to the translator API is gated behind the [=policy-controlled feature=] "translator", which has a [=policy-controlled feature/default allowlist=] of [=default allowlist/'self'=]
.
469 |
470 | The language detector API
471 |
472 |
473 | [Exposed=Window, SecureContext]
474 | interface LanguageDetector {
475 | static Promise create(
476 | optional LanguageDetectorCreateOptions options = {}
477 | );
478 | static Promise availability(
479 | optional LanguageDetectorCreateCoreOptions options = {}
480 | );
481 |
482 | Promise> detect(
483 | DOMString input,
484 | optional LanguageDetectorDetectOptions options = {}
485 | );
486 |
487 | readonly attribute FrozenArray? expectedInputLanguages;
488 |
489 | Promise measureInputUsage(
490 | DOMString input,
491 | optional LanguageDetectorDetectOptions options = {}
492 | );
493 | readonly attribute unrestricted double inputQuota;
494 | };
495 | LanguageDetector includes DestroyableModel;
496 |
497 | dictionary LanguageDetectorCreateCoreOptions {
498 | sequence expectedInputLanguages;
499 | };
500 |
501 | dictionary LanguageDetectorCreateOptions : LanguageDetectorCreateCoreOptions {
502 | AbortSignal signal;
503 | CreateMonitorCallback monitor;
504 | };
505 |
506 | dictionary LanguageDetectorDetectOptions {
507 | AbortSignal signal;
508 | };
509 |
510 | dictionary LanguageDetectionResult {
511 | DOMString detectedLanguage;
512 | double confidence;
513 | };
514 |
515 |
516 | Creation
517 |
518 |
519 | The static create(|options|) method steps are:
520 |
521 | 1. Return the result of [=creating an AI model object=] given |options|, "{{language-detector}}", [=validate and canonicalize language detector options=], [=compute language detector options availability=], [=download the language detector model=], [=initialize the language detector model=], and [=create the language detector object=].
522 |
523 |
524 |
525 | To validate and canonicalize language detector options given a {{LanguageDetectorCreateCoreOptions}} |options|, perform the following steps. They mutate |options| in place to canonicalize language tags, and throw an exception if any are invalid.
526 |
527 | 1. [=Validate and canonicalize language tags=] given |options| and "{{LanguageDetectorCreateCoreOptions/expectedInputLanguages}}".
528 |
529 |
530 |
531 | To download the language detector model, given a {{LanguageDetectorCreateCoreOptions}} |options|:
532 |
533 | 1. [=Assert=]: these steps are running [=in parallel=].
534 |
535 | 1. Initiate the download process for everything the user agent needs to detect the languages of input text, including all the languages in |options|["{{LanguageDetectorCreateCoreOptions/expectedInputLanguages}}"].
536 |
537 | This could include both a base language detection model, and specific fine-tunings or other material to help with the languages identified in |options|["{{LanguageDetectorCreateCoreOptions/expectedInputLanguages}}"].
538 |
539 | 1. If the download process cannot be started for any reason, then return false.
540 |
541 | 1. Return true.
542 |
543 |
544 |
545 | To initialize the language detector model, given a {{LanguageDetectorCreateCoreOptions}} |options|:
546 |
547 | 1. [=Assert=]: these steps are running [=in parallel=].
548 |
549 | 1. Perform any necessary initialization operations for the AI model backing the user agent's capabilities for detecting the languages of input text.
550 |
551 | This could include loading the model into memory, or loading any fine-tunings necessary to support the languages identified in |options|["{{LanguageDetectorCreateCoreOptions/expectedInputLanguages}}"].
552 |
553 | 1. If initialization failed for any reason, then return a [=DOMException error information=] whose [=DOMException error information/name=] is "{{OperationError}}" and whose [=DOMException error information/details=] contain appropriate detail.
554 |
555 | 1. Return null.
556 |
557 |
558 |
559 | To
create the language detector object, given a [=ECMAScript/realm=] |realm| and a {{LanguageDetectorCreateCoreOptions}} |options|:
560 |
561 | 1. [=Assert=]: these steps are running on |realm|'s [=ECMAScript/surrounding agent=]'s [=agent/event loop=].
562 |
563 | 1. Let |inputQuota| be the amount of input quota that is available to the user agent for future [=detect languages|language detection=] operations. (This value is [=implementation-defined=], and may be +∞ if there are no specific limits beyond, e.g., the user's memory, or the limits of JavaScript strings.)
564 |
565 | 1. Return a new {{LanguageDetector}} object, created in |realm|, with
566 |
567 |
568 | : [=LanguageDetector/expected input languages=]
569 | :: the result of [=creating a frozen array=] given |options|["{{LanguageDetectorCreateCoreOptions/expectedInputLanguages}}"] if it [=set/is empty|is not empty=]; otherwise null
570 |
571 | : [=LanguageDetector/input quota=]
572 | :: |inputQuota|
573 |
574 |
575 |
576 | Availability
577 |
578 |
579 | The static availability(|options|) method steps are:
580 |
581 | 1. Return the result of [=computing AI model availability=] given |options|, "{{language-detector}}", [=validate and canonicalize language detector options=], and [=compute language detector options availability=].
582 |
583 |
584 |
585 | To compute language detector options availability given a {{LanguageDetectorCreateCoreOptions}} |options|, perform the following steps. They return either an {{Availability}} value or null, and they mutate |options| in place to update language tags to their best-fit matches.
586 |
587 | 1. [=Assert=]: this algorithm is running [=in parallel=].
588 |
589 | 1. If there is some error attempting to determine what language detection capabilities the user agent [=model availability/can support=], which the user agent believes to be transient (such that re-querying could stop producing such an error), then return null.
590 |
591 | 1. Let |partition| be the result of [=getting the language availabilities partition=] given the purpose of detecting text written in that language.
592 |
593 | 1. Return the result of [=computing language availability=] given |options|["{{LanguageDetectorCreateCoreOptions/expectedInputLanguages}}"] and |partition|.
594 |
595 |
596 | The {{LanguageDetector}} class
597 |
598 | Every {{LanguageDetector}} has an expected input languages, a {{FrozenArray}}<{{DOMString}}>
or null, set during creation.
599 |
600 | Every {{LanguageDetector}} has an input quota, a number, set during creation.
601 |
602 |
603 |
604 | The expectedInputLanguages getter steps are to return [=this=]'s [=LanguageDetector/expected input languages=].
605 |
606 | The inputQuota getter steps are to return [=this=]'s [=LanguageDetector/input quota=].
607 |
608 |
609 |
610 |
611 | The
detect(|input|, |options|) method steps are:
612 |
613 | 1. Let |global| be [=this=]'s [=relevant global object=].
614 |
615 | 1. [=Assert=]: |global| is a {{Window}} object.
616 |
617 | 1. If |global|'s [=associated Document=] is not [=Document/fully active=], then return [=a promise rejected with=] an "{{InvalidStateError}}" {{DOMException}}.
618 |
619 | 1. Let |signals| be « [=this=]'s [=DestroyableModel/destruction abort controller=]'s [=AbortController/signal=] ».
620 |
621 | 1. If |options|["`signal`"] [=map/exists=], then [=set/append=] it to |signals|.
622 |
623 | 1. Let |compositeSignal| be the result of [=creating a dependent abort signal=] given |signals| using {{AbortSignal}} and [=this=]'s [=relevant realm=].
624 |
625 | 1. If |compositeSignal| is [=AbortSignal/aborted=], then return [=a promise rejected with=] |compositeSignal|'s [=AbortSignal/abort reason=].
626 |
627 | 1. Let |promise| be [=a new promise=] created in [=this=]'s [=relevant realm=].
628 |
629 | 1. Let |abortedDuringOperation| be false.
630 |
631 |
This variable will be written to from the [=event loop=], but read from [=in parallel=].
632 |
633 | 1. [=AbortSignal/add|Add the following abort steps=] to |compositeSignal|:
634 |
635 | 1. Set |abortedDuringOperation| to true.
636 |
637 | 1. [=Reject=] |promise| with |compositeSignal|'s [=AbortSignal/abort reason=].
638 |
639 | 1. Let |inputQuota| be [=this=]'s [=LanguageDetector/input quota=].
640 |
641 | 1. [=In parallel=]:
642 |
643 | 1. Let |stopProducing| be the following steps:
644 |
645 | 1. Return |abortedDuringOperation|.
646 |
647 | 1. Let |result| be the result of [=detecting languages=] given |input|, |inputQuota|, and |stopProducing|.
648 |
649 | 1. [=Queue a global task=] on the [=AI task source=] given |global| to perform the following steps:
650 |
651 | 1. If |abortedDuringOperation| is true, then abort these steps.
652 |
653 | 1. Otherwise, if |result| is an [=error information=], then [=reject=] |promise| with the result of [=converting error information into an exception object=] given |result|.
654 |
655 | 1. Otherwise:
656 |
657 | 1. [=Assert=]: |result| is a [=list=] of {{LanguageDetectionResult}} dictionaries. (It is not null, since in that case |abortedDuringOperation| would have been true.)
658 |
659 | 1. [=Resolve=] |promise| with |result|.
660 |
661 |
662 |
663 | The measureInputUsage(|input|, |options|) method steps are:
664 |
665 | 1. Let |measureUsage| be an algorithm step which takes argument |stopMeasuring|, and returns the result of [=measuring language detector input usage=] given |input| and |stopMeasuring|.
666 |
667 | 1. Return the result of [=measuring AI model input usage=] given [=this=], |options|, and |measureUsage|.
668 |
669 |
670 | Language detection
671 |
672 | The algorithm
673 |
674 |
675 | To
detect languages given a [=string=] |input|, a number |inputQuota|, and an algorithm |stopProducing| that takes no arguments and returns a boolean, perform the following steps. They will return either null, an [=error information=], or a [=list=] of {{LanguageDetectionResult}} dictionaries.
676 |
677 | 1. [=Assert=]: this algorithm is running [=in parallel=].
678 |
679 | 1. Let |requested| be the result of [=measuring language detector input usage=] given |input| and |stopProducing|.
680 |
681 | 1. If |requested| is null or an [=error information=], then return |requested|.
682 |
683 | 1. [=Assert=]: |requested| is a number.
684 |
685 | 1. If |requested| is greater than |inputQuota|, then return a [=quota exceeded error information=] with a [=quota exceeded error information/requested=] of |requested| and a [=quota exceeded error information/quota=] of |inputQuota|.
686 |
687 |
In reality, we expect that implementations will check the input usage against the quota as part of the same call into the model as the language detection itself. The steps are only separated in the specification for ease of understanding.
688 |
689 | 1. Let |partition| be the result of [=getting the language availabilities partition=] given the purpose of detecting text written in that language.
690 |
691 | 1. Let |currentlyAvailableLanguages| be |partition|["{{Availability/available}}"].
692 |
693 | 1. In an [=implementation-defined=] manner, subject to the following guidelines, let |rawResult| and |unknown| be the result of detecting the languages of |input|.
694 |
695 | |rawResult| must be a [=map=] which has a [=map/key=] for each language in |currentlyAvailableLanguages|. The [=map/value=] for each such key must be a number between 0 and 1. This value must represent the implementation's confidence that |input| is written in that language.
696 |
697 | |unknown| must be a number between 0 and 1 that represents the implementation's confidence that |input| is not written in any of the languages in |currentlyAvailableLanguages|.
698 |
699 | The [=map/values=] of |rawResult|, plus |unknown|, must sum to 1. Each such value, or |unknown|, may be 0.
700 |
701 | If the implementation believes |input| to be written in multiple languages, then it should attempt to apportion the values of |rawResult| and |unknown| such that they are proportionate to the amount of |input| written in each detected language. The exact scheme for apportioning |input| is [=implementation-defined=].
702 |
703 |
710 |
711 | If |stopProducing| returns true at any point during this process, then return null.
712 |
713 | If an error occurred during language detection, then return an [=error information=] according to the guidance in [[#language-detector-errors]].
714 |
715 | The detection process must conform to the guidance given in [[WRITING-ASSISTANCE-APIS#privacy]] and [[WRITING-ASSISTANCE-APIS#security]], notably including (but not limited to) [[WRITING-ASSISTANCE-APIS#privacy-user-input]] and [[WRITING-ASSISTANCE-APIS#security-runtime]].
716 |
717 | 1. [=map/Sort in descending order=] |rawResult| with a less than algorithm which given [=map/entries=] |a| and |b|, returns true if |a|'s [=map/value=] is less than |b|'s [=map/value=].
718 |
719 | 1. Let |results| be an empty [=list=].
720 |
721 | 1. Let |cumulativeConfidence| be 0.
722 |
723 | 1. [=map/For each=] |key| → |value| of |rawResult|:
724 |
725 | 1. If |value| is 0, then [=iteration/break=].
726 |
727 | 1. If |value| is less than |unknown|, then [=iteration/break=].
728 |
729 | 1. [=list/Append=] «[ "{{LanguageDetectionResult/detectedLanguage}}" → |key|, "{{LanguageDetectionResult/confidence}}" → |value| ]» to |results|.
730 |
731 | 1. Set |cumulativeConfidence| to |cumulativeConfidence| + |value|.
732 |
733 | 1. If |cumulativeConfidence| is greater than or equal to 0.99, then [=iteration/break=].
734 |
735 | 1. [=Assert=]: 1 − |cumulativeConfidence| is greater than or equal to |unknown|.
736 |
737 | 1. [=Assert=]: If |results|'s [=list/size=] is greater than 0, then |results|[|results|'s [=list/size=] - 1]["{{LanguageDetectionResult/confidence}}"] is greater than or equal to |unknown|.
738 |
739 | 1. [=list/Append=] «[ "{{LanguageDetectionResult/detectedLanguage}}" → "`und`", "{{LanguageDetectionResult/confidence}}" → |unknown| ]» to |results|.
740 |
741 | 1. Return |results|.
742 |
743 |
Languages which are less than 1% likely, or contribute to less than 1% of the text, are considered more likely to be noise and so not worth returning to the web developer. Similarly, if the implementation is less sure about a language than it is about the text not being in any of the languages it knows, that language is probably not worth returning to the web developer.
744 |
745 |
Because of such omitted low-probability results, the sum of all confidence values returned to the web developer could be less than 1.
746 |
747 |
748 | Usage
749 |
750 |
751 | To
measure language detector input usage, given a [=string=] |input| and an algorithm |stopMeasuring| that takes no arguments and returns a boolean, perform the following steps:
752 |
753 | 1. [=Assert=]: this algorithm is running [=in parallel=].
754 |
755 | 1. Let |inputToModel| be the [=implementation-defined=] string that would be sent to the underlying model in order to [=detect languages=] given |input|.
756 |
757 |
This might be just |input| itself, or it might include some sort of wrapper prompt to a language model.
758 |
759 | If during this process |stopMeasuring| starts returning true, then return null.
760 |
761 | If an error occurs during this process, then return an appropriate [=DOMException error information=] according to the guidance in [[#language-detector-errors]].
762 |
763 | 1. Return the amount of input usage needed to represent |inputToModel| when given to the underlying model. The exact calculation procedure is [=implementation-defined=], subject to the following constraints.
764 |
765 | The returned input usage must be nonnegative and finite. It must be 0, if there are no usage quotas for the translation process (i.e., if the [=LanguageDetector/input quota=] is +∞). Otherwise, it must be positive and should be roughly proportional to the [=string/length=] of |inputToModel|.
766 |
767 |
This might be the number of tokens needed to represent |input| in a language model tokenization scheme, or it might be |input|'s [=string/length=]. It could also be some variation of these which also counts the usage of any prefixes or suffixes necessary to give to the model.
768 |
769 | If during this process |stopMeasuring| starts returning true, then instead return null.
770 |
771 | If an error occurs during this process, then instead return an appropriate [=DOMException error information=] according to the guidance in [[#language-detector-errors]].
772 |
773 |
774 | Errors
775 |
776 | When language detection fails, the following possible reasons may be surfaced to the web developer. This table lists the possible {{DOMException}} [=DOMException/names=] and the cases in which an implementation should use them:
777 |
778 |
779 |
780 |
781 | {{DOMException}} [=DOMException/name=]
782 | | Scenarios
783 | |
784 |
785 | "{{NotAllowedError}}"
786 | |
787 | Language detection is disabled by user choice or user agent policy.
788 | |
789 | "{{UnknownError}}"
790 | |
791 | All other scenarios, including if the user agent believes it cannot detect and also meet the requirements given in [[WRITING-ASSISTANCE-APIS#privacy]] and [[WRITING-ASSISTANCE-APIS#security]]. Or, if the user agent would prefer not to disclose the failure reason.
792 | |
793 |
794 | This table does not give the complete list of exceptions that can be surfaced by the language detector API. It only contains those which can come from certain [=implementation-defined=] steps.
795 |
796 |
Permissions policy integration
797 |
798 | Access to the language detector API is gated behind the [=policy-controlled feature=] "language-detector", which has a [=policy-controlled feature/default allowlist=] of [=default allowlist/'self'=]
.
799 |
800 | Privacy considerations
801 |
802 | Please see [[WRITING-ASSISTANCE-APIS#privacy]] for a discussion of privacy considerations for the translator and language detector APIs. That text was written to apply to all APIs sharing the same infrastructure, as noted in [[#dependencies]].
803 |
804 | Security considerations
805 |
806 | Please see [[WRITING-ASSISTANCE-APIS#security]] for a discussion of security considerations for the translator and language detector APIs. That text was written to apply to all APIs sharing the same infrastructure, as noted in [[#dependencies]].
807 |
--------------------------------------------------------------------------------
/security-privacy-questionnaire.md:
--------------------------------------------------------------------------------
1 | # [Self-Review Questionnaire: Security and Privacy](https://w3ctag.github.io/security-questionnaire/)
2 |
3 | > 01. What information does this feature expose,
4 | > and for what purposes?
5 |
6 | This feature exposes two main pieces of information:
7 |
8 | - The availability information for each `(sourceLanguage, targetLanguage)` translation pair, or possible language detection result, so that web developers know what translations and detections are possible and whether they will require the user to download a potentially-large language pack.
9 |
10 | (This information has to be probed for each individual pair or possible language, and the browser can say that a language is unavailable even if it is, for privacy reasons.)
11 |
12 | - The actual results of translations and language detections, which can be dependent on the AI models in use.
13 |
14 | The privacy implications of both of these are discussed, in general terms, [in the _Writing Assistance APIs_ specification](https://webmachinelearning.github.io/writing-assistance-apis/#privacy), which was written to cover all APIs with similar concerns.
15 |
16 | > 02. Do features in your specification expose the minimum amount of information
17 | > necessary to implement the intended functionality?
18 |
19 | We believe so. It's possible that we could remove the exposure of the download status information. However, it would almost certainly be inferrable via timing side-channels. (I.e., if downloading a language pack is required, then the web developer can observe the first translation taking longer.)
20 |
21 | > 03. Do the features in your specification expose personal information,
22 | > personally-identifiable information (PII), or information derived from
23 | > either?
24 |
25 | No. Although it's imaginable that the translation or language detection models could be fine-tuned on PII to give more accurate-to-this-user translations, we intend to disallow this in the specification.
26 |
27 | > 04. How do the features in your specification deal with sensitive information?
28 |
29 | We do not deal with sensitive information.
30 |
31 | > 05. Do the features in your specification introduce state
32 | > that persists across browsing sessions?
33 |
34 | Yes. The downloading of language packs and translation or language detection models persists across browsing sessions.
35 |
36 | > 06. Do the features in your specification expose information about the
37 | > underlying platform to origins?
38 |
39 | Possibly. If a browser does not bundle its own models, but instead uses the operating system's functionality, it is possible for a web developer to infer information about such operating system functionality.
40 |
41 | > 07. Does this specification allow an origin to send data to the underlying
42 | > platform?
43 |
44 | Possibly. Again, in the scenario where translation is done via operating system functionality, such data would pass through OS libraries.
45 |
46 | > 08. Do features in this specification enable access to device sensors?
47 |
48 | No.
49 |
50 | > 09. Do features in this specification enable new script execution/loading
51 | > mechanisms?
52 |
53 | No.
54 |
55 | > 10. Do features in this specification allow an origin to access other devices?
56 |
57 | No.
58 |
59 | > 11. Do features in this specification allow an origin some measure of control over
60 | > a user agent's native UI?
61 |
62 | No.
63 |
64 | > 12. What temporary identifiers do the features in this specification create or
65 | > expose to the web?
66 |
67 | None.
68 |
69 | > 13. How does this specification distinguish between behavior in first-party and
70 | > third-party contexts?
71 |
72 | We use permissions policy to disallow the usage of these features by default in third-party (cross-origin) contexts. However, the top-level site can delegate to cross-origin iframes.
73 |
74 | Otherwise, some of the possible [anti-fingerprinting mitigations](https://webmachinelearning.github.io/writing-assistance-apis/#privacy-availability) involve partitioning information across sites, which is kind of like distinguishing between first- and third-party contexts.
75 |
76 | > 14. How do the features in this specification work in the context of a browser’s
77 | > Private Browsing or Incognito mode?
78 |
79 | We are not yet sure. It is likely that some behavior will be different to deal with the [anti-fingerprinting considerations](./README.md#privacy-considerations). For example, if storage partitioning infrastructure is used, then this would be automatic.
80 |
81 | Another possible area of discussion here is whether cloud-based translation APIs make sense in such modes, or whether they should be disabled.
82 |
83 | > 15. Does this specification have both "Security Considerations" and "Privacy
84 | > Considerations" sections?
85 |
86 | Yes:
87 |
88 | * [Privacy considerations](https://webmachinelearning.github.io/translation-api/#privacy) (delegates to [the corresponding section in _Writing Assistance APIs_](https://webmachinelearning.github.io/writing-assistance-apis/#privacy))
89 | * [Security considerations](https://webmachinelearning.github.io/translation-api/#security) (delegates to [the corresponding section in _Writing Assistance APIs_](https://webmachinelearning.github.io/writing-assistance-apis/#security))
90 |
91 | > 16. Do features in your specification enable origins to downgrade default
92 | > security protections?
93 |
94 | No.
95 |
96 | > 17. What happens when a document that uses your feature is kept alive in BFCache
97 | > (instead of getting destroyed) after navigation, and potentially gets reused
98 | > on future navigations back to the document?
99 |
100 | Ideally, nothing special should happen. In particular, `Translator` and `LanguageDetector` objects should still be usable without interruption after navigating back. We'll need to add web platform tests to confirm this, as it's easy to imagine implementation architectures in which keeping these objects alive while the `Document` is in the back/forward cache is difficult.
101 |
102 | (For such implementations, failing to bfcache `Document`s with active `Translator` or `LanguageDetector` objects would a simple way of being spec-compliant.)
103 |
104 | > 18. What happens when a document that uses your feature gets disconnected?
105 |
106 | The methods of the `Translator`/`LanguageDetector` objects will start rejecting with `"InvalidStateError"` `DOMException`s.
107 |
108 | > 19. What should this questionnaire have asked?
109 |
110 | Seems fine.
111 |
--------------------------------------------------------------------------------
/w3c.json:
--------------------------------------------------------------------------------
1 | {
2 | "group": [110166]
3 | , "contacts": ["cwilso"]
4 | , "repo-type": "cg-report"
5 | }
6 |
--------------------------------------------------------------------------------