@backstage/core-plugin-api 1.6.0-next.3 → 1.6.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @backstage/core-plugin-api
2
2
 
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 18619f793c94: Added the optional `expiresAt` field that may now be part of a `BackstageIdentityResponse`.
8
+ - 6e30769cc627: Introduced experimental support for internationalization.
9
+
10
+ ### Patch Changes
11
+
12
+ - 406b786a2a2c: Mark package as being free of side effects, allowing more optimized Webpack builds.
13
+ - 8cec7664e146: Removed `@types/node` dependency
14
+ - Updated dependencies
15
+ - @backstage/config@1.1.0
16
+ - @backstage/types@1.1.1
17
+ - @backstage/version-bridge@1.0.5
18
+
3
19
  ## 1.6.0-next.3
4
20
 
5
21
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/core-plugin-api",
3
- "version": "1.6.0-next.3",
3
+ "version": "1.6.0",
4
4
  "main": "../dist/alpha.esm.js",
5
5
  "module": "../dist/alpha.esm.js",
6
6
  "types": "../dist/alpha.d.ts"
package/dist/alpha.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { BackstagePlugin, ApiRef } from '@backstage/core-plugin-api';
2
2
  import { ReactNode } from 'react';
3
- import { i18n } from 'i18next';
3
+ import { TranslationRef as TranslationRef$1, TranslationMessages as TranslationMessages$1 } from '@backstage/core-plugin-api/alpha';
4
+ import { Observable } from '@backstage/types';
4
5
 
5
6
  /**
6
7
  * Properties for the PluginProvider component.
@@ -25,54 +26,393 @@ declare const PluginProvider: (props: PluginOptionsProviderProps) => JSX.Element
25
26
  */
26
27
  declare function usePluginOptions<TPluginOptions extends {} = {}>(): TPluginOptions;
27
28
 
29
+ /**
30
+ * Represents a collection of messages to be provided for a given translation ref.
31
+ *
32
+ * @alpha
33
+ * @remarks
34
+ *
35
+ * This collection of messages can either be used directly as an override for the
36
+ * default messages, or it can be used to provide translations for a language by
37
+ * by being referenced by a {@link TranslationResource}.
38
+ */
39
+ interface TranslationMessages<TId extends string = string, TMessages extends {
40
+ [key in string]: string;
41
+ } = {
42
+ [key in string]: string;
43
+ }, TFull extends boolean = boolean> {
44
+ $$type: '@backstage/TranslationMessages';
45
+ /** The ID of the translation ref that these messages are for */
46
+ id: TId;
47
+ /** Whether or not these messages override all known messages */
48
+ full: TFull;
49
+ /** The messages provided for the given translation ref */
50
+ messages: TMessages;
51
+ }
52
+ /**
53
+ * Options for {@link createTranslationMessages}.
54
+ *
55
+ * @alpha
56
+ */
57
+ interface TranslationMessagesOptions<TId extends string, TMessages extends {
58
+ [key in string]: string;
59
+ }, TFull extends boolean> {
60
+ ref: TranslationRef$1<TId, TMessages>;
61
+ full?: TFull;
62
+ messages: false extends TFull ? {
63
+ [key in keyof TMessages]?: string | null;
64
+ } : {
65
+ [key in keyof TMessages]: string | null;
66
+ };
67
+ }
68
+ /**
69
+ * Creates a collection of messages for a given translation ref.
70
+ *
71
+ * @alpha
72
+ */
73
+ declare function createTranslationMessages<TId extends string, TMessages extends {
74
+ [key in string]: string;
75
+ }, TFull extends boolean>(options: TranslationMessagesOptions<TId, TMessages, TFull>): TranslationMessages<TId, TMessages, TFull>;
76
+
28
77
  /** @alpha */
29
- interface TranslationRefConfig<Messages extends Record<keyof Messages, string>> {
30
- id: string;
31
- messages: Messages;
32
- lazyResources?: Record<string, () => Promise<{
33
- messages: Messages;
34
- }>>;
35
- resources?: Record<string, Messages>;
78
+ interface TranslationResource<TId extends string = string> {
79
+ $$type: '@backstage/TranslationResource';
80
+ id: TId;
36
81
  }
37
82
  /** @alpha */
38
- interface TranslationRef<Messages extends Record<keyof Messages, string> = Record<string, string>> {
39
- getId(): string;
40
- getDefaultMessages(): Messages;
41
- getResources(): Record<string, Messages> | undefined;
42
- getLazyResources(): Record<string, () => Promise<{
43
- messages: Messages;
44
- }>> | undefined;
83
+ interface TranslationResourceOptions<TId extends string, TMessages extends {
84
+ [key in string]: string;
85
+ }, TTranslations extends {
86
+ [language in string]: () => Promise<{
87
+ default: TranslationMessages$1<TId> | {
88
+ [key in keyof TMessages]: string | null;
89
+ };
90
+ }>;
91
+ }> {
92
+ ref: TranslationRef$1<TId, TMessages>;
93
+ translations: TTranslations;
45
94
  }
46
95
  /** @alpha */
47
- type TranslationOptions<Messages extends Record<keyof Messages, string> = Record<string, string>> = Messages;
96
+ declare function createTranslationResource<TId extends string, TMessages extends {
97
+ [key in string]: string;
98
+ }, TTranslations extends {
99
+ [language in string]: () => Promise<{
100
+ default: TranslationMessages$1<TId> | {
101
+ [key in keyof TMessages]: string | null;
102
+ };
103
+ }>;
104
+ }>(options: TranslationResourceOptions<TId, TMessages, TTranslations>): TranslationResource<TId>;
48
105
 
49
106
  /** @alpha */
50
- declare class TranslationRefImpl<Messages extends Record<keyof Messages, string>> implements TranslationRef<Messages> {
51
- private readonly config;
52
- static create<Messages extends Record<keyof Messages, string>>(config: TranslationRefConfig<Messages>): TranslationRefImpl<Messages>;
53
- getId(): string;
54
- getDefaultMessages(): Messages;
55
- getLazyResources(): Record<string, () => Promise<{
56
- messages: Messages;
57
- }>> | undefined;
58
- getResources(): Record<string, Messages> | undefined;
59
- toString(): string;
60
- private constructor();
107
+ interface TranslationRef<TId extends string = string, TMessages extends {
108
+ [key in string]: string;
109
+ } = {
110
+ [key in string]: string;
111
+ }> {
112
+ $$type: '@backstage/TranslationRef';
113
+ id: TId;
114
+ T: TMessages;
61
115
  }
62
116
  /** @alpha */
63
- declare const createTranslationRef: <Messages extends Record<keyof Messages, string> = {}>(config: TranslationRefConfig<Messages>) => TranslationRef<Messages>;
117
+ interface TranslationRefOptions<TId extends string, TMessages extends {
118
+ [key in string]: string;
119
+ }, TTranslations extends {
120
+ [language in string]: () => Promise<{
121
+ default: {
122
+ [key in keyof TMessages]: string | null;
123
+ };
124
+ }>;
125
+ }> {
126
+ id: TId;
127
+ messages: TMessages;
128
+ translations?: TTranslations;
129
+ }
130
+ /** @alpha */
131
+ declare function createTranslationRef<TId extends string, const TMessages extends {
132
+ [key in string]: string;
133
+ }, TTranslations extends {
134
+ [language in string]: () => Promise<{
135
+ default: {
136
+ [key in keyof TMessages]: string | null;
137
+ };
138
+ }>;
139
+ }>(config: TranslationRefOptions<TId, TMessages, TTranslations>): TranslationRef<TId, TMessages>;
64
140
 
141
+ /**
142
+ * Base translation options.
143
+ *
144
+ * @alpha
145
+ */
146
+ interface BaseOptions {
147
+ interpolation?: {
148
+ /** Whether to HTML escape provided values, defaults to false */
149
+ escapeValue?: boolean;
150
+ };
151
+ }
152
+ /**
153
+ * All pluralization suffixes supported by i18next
154
+ *
155
+ * @ignore
156
+ */
157
+ type TranslationPlural = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
158
+ /**
159
+ * A mapping of i18n formatting types to their corresponding types and options.
160
+ * @ignore
161
+ */
162
+ type I18nextFormatMap = {
163
+ number: {
164
+ type: number;
165
+ options: Intl.NumberFormatOptions;
166
+ };
167
+ currency: {
168
+ type: number;
169
+ options: Intl.NumberFormatOptions;
170
+ };
171
+ datetime: {
172
+ type: Date;
173
+ options: Intl.DateTimeFormatOptions;
174
+ };
175
+ relativetime: {
176
+ type: number;
177
+ options: {
178
+ range?: Intl.RelativeTimeFormatUnit;
179
+ } & Intl.RelativeTimeFormatOptions;
180
+ };
181
+ list: {
182
+ type: string[];
183
+ options: Intl.ListFormatOptions;
184
+ };
185
+ };
186
+ /**
187
+ * Extracts all pluralized keys from the message map.
188
+ *
189
+ * @example
190
+ * ```
191
+ * { foo: 'foo', bar_one: 'bar', bar_other: 'bars' } -> 'bar'
192
+ * ```
193
+ *
194
+ * @ignore
195
+ */
196
+ type PluralKeys<TMessages extends {
197
+ [key in string]: string;
198
+ }> = {
199
+ [Key in keyof TMessages]: Key extends `${infer K}_${TranslationPlural}` ? K : never;
200
+ }[keyof TMessages];
201
+ /**
202
+ * Collapses a message map into normalized keys with union values.
203
+ *
204
+ * @example
205
+ * ```
206
+ * { foo_one: 'foo', foo_other: 'foos' } -> { foo: 'foo' | 'foos' }
207
+ * ```
208
+ *
209
+ * @ignore
210
+ */
211
+ type CollapsedMessages<TMessages extends {
212
+ [key in string]: string;
213
+ }> = {
214
+ [key in keyof TMessages as key extends `${infer K}_${TranslationPlural}` ? K : key]: TMessages[key];
215
+ };
216
+ /**
217
+ * Helper type that expands type hints
218
+ *
219
+ * @ignore
220
+ */
221
+ type Expand<T> = T extends infer O ? {
222
+ [K in keyof O]: O[K];
223
+ } : never;
224
+ /**
225
+ * Helper type that expands type hints recursively
226
+ *
227
+ * @ignore
228
+ */
229
+ type ExpandRecursive<T> = T extends infer O ? {
230
+ [K in keyof O]: ExpandRecursive<O[K]>;
231
+ } : never;
232
+ /**
233
+ * Trim away whitespace
234
+ *
235
+ * @ignore
236
+ */
237
+ type Trim<T> = T extends ` ${infer U}` ? Trim<U> : T extends `${infer U} ` ? Trim<U> : T;
238
+ /**
239
+ * Extracts the key and format from a replacement string.
240
+ *
241
+ * @example
242
+ * ```
243
+ * 'foo, number' -> { foo: number }, 'foo' -> { foo: undefined }
244
+ * ```
245
+ */
246
+ type ExtractFormat<Replacement extends string> = Replacement extends `${infer Key},${infer FullFormat}` ? {
247
+ [key in Trim<Key>]: Lowercase<Trim<FullFormat extends `${infer Format}(${string})${string}` ? Format : FullFormat>>;
248
+ } : {
249
+ [key in Trim<Replacement>]: undefined;
250
+ };
251
+ /**
252
+ * Expand the keys in a flat map to nested objects.
253
+ *
254
+ * @example
255
+ * ```
256
+ * { 'a.b': 'foo', 'a.c': 'bar' } -> { a: { b: 'foo', c: 'bar' }
257
+ * ```
258
+ *
259
+ * @ignore
260
+ */
261
+ type ExpandKeys<TMap extends {}> = {
262
+ [Key in keyof TMap as Key extends `${infer Prefix}.${string}` ? Prefix : Key]: Key extends `${string}.${infer Rest}` ? ExpandKeys<{
263
+ [key in Rest]: TMap[Key];
264
+ }> : TMap[Key];
265
+ };
266
+ /**
267
+ * Extracts all option keys and their format from a message string.
268
+ *
269
+ * @example
270
+ * ```
271
+ * 'foo {{bar}} {{baz, number}}' -> { 'bar': undefined, 'baz': 'number' }
272
+ * ```
273
+ *
274
+ * @ignore
275
+ */
276
+ type ReplaceFormatsFromMessage<TMessage> = TMessage extends `${string}{{${infer Replacement}}}${infer Tail}` ? ExpandKeys<ExtractFormat<Replacement>> & ReplaceFormatsFromMessage<Tail> : {};
277
+ /**
278
+ * Generates the replace options structure
279
+ *
280
+ * @ignore
281
+ */
282
+ type ReplaceOptionsFromFormats<TFormats extends {}> = {
283
+ [Key in keyof TFormats]: TFormats[Key] extends keyof I18nextFormatMap ? I18nextFormatMap[TFormats[Key]]['type'] : TFormats[Key] extends {} ? Expand<ReplaceOptionsFromFormats<TFormats[Key]>> : string;
284
+ };
285
+ /**
286
+ * Generates the formatParams options structure
287
+ *
288
+ * @ignore
289
+ */
290
+ type ReplaceFormatParamsFromFormats<TFormats extends {}> = {
291
+ [Key in keyof TFormats]?: TFormats[Key] extends keyof I18nextFormatMap ? I18nextFormatMap[TFormats[Key]]['options'] : TFormats[Key] extends {} ? Expand<ReplaceFormatParamsFromFormats<TFormats[Key]>> : undefined;
292
+ };
293
+ /**
294
+ * Extracts all nesting keys from a message string.
295
+ *
296
+ * @example
297
+ * ```
298
+ * 'foo $t(bar) $t(baz)' -> 'bar' | 'baz'
299
+ * ```
300
+ *
301
+ * @ignore
302
+ */
303
+ type NestingKeysFromMessage<TMessage extends string> = TMessage extends `${string}$t(${infer Key})${infer Tail}` ? Trim<Key> | NestingKeysFromMessage<Tail> : never;
304
+ /**
305
+ * Find all referenced keys, given a starting key and the full set of messages.
306
+ *
307
+ * This will only discover keys up to 3 levels deep.
308
+ *
309
+ * @example
310
+ * ```
311
+ * <'x', { x: '$t(y) $t(z)', y: 'y', z: '$t(w)', w: 'w', foo: 'foo' }> -> 'x' | 'y' | 'z' | 'w'
312
+ * ```
313
+ *
314
+ * @ignore
315
+ */
316
+ type NestedMessageKeys<TKey extends keyof TMessages, TMessages extends {
317
+ [key in string]: string;
318
+ }> = TKey | NestedMessageKeys2<NestingKeysFromMessage<TMessages[TKey]>, TMessages>;
319
+ type NestedMessageKeys2<TKey extends keyof TMessages, TMessages extends {
320
+ [key in string]: string;
321
+ }> = TKey | NestedMessageKeys3<NestingKeysFromMessage<TMessages[TKey]>, TMessages>;
322
+ type NestedMessageKeys3<TKey extends keyof TMessages, TMessages extends {
323
+ [key in string]: string;
324
+ }> = TKey | NestingKeysFromMessage<TMessages[TKey]>;
325
+ /**
326
+ * Converts a union type to an intersection type.
327
+ *
328
+ * @example
329
+ * ```
330
+ * { foo: 'foo' } | { bar: 'bar' } -> { foo: 'foo' } & { bar: 'bar' }
331
+ * ```
332
+ *
333
+ * @ignore
334
+ */
335
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
336
+ /**
337
+ * Collects different types of options into a single object
338
+ *
339
+ * @ignore
340
+ */
341
+ type CollectOptions<TCount extends {
342
+ count?: number;
343
+ }, TFormats extends {}> = TCount & (keyof Omit<TFormats, 'count'> extends never ? {} : (Expand<Omit<ReplaceOptionsFromFormats<TFormats>, 'count'>> | {
344
+ replace: Expand<Omit<ReplaceOptionsFromFormats<TFormats>, 'count'>>;
345
+ }) & {
346
+ formatParams?: Expand<ReplaceFormatParamsFromFormats<TFormats>>;
347
+ });
348
+ /**
349
+ * Helper type to only require options argument if needed
350
+ *
351
+ * @ignore
352
+ */
353
+ type OptionArgs<TOptions extends {}> = keyof TOptions extends never ? [options?: BaseOptions] : [options: BaseOptions & TOptions];
354
+ /**
355
+ * @ignore
356
+ */
357
+ type TranslationFunctionOptions<TKeys extends keyof TMessages, // All normalized message keys to be considered, i.e. included nested ones
358
+ TPluralKeys extends keyof TMessages, // All keys in the message map that are pluralized
359
+ TMessages extends {
360
+ [key in string]: string;
361
+ }> = OptionArgs<Expand<CollectOptions<TKeys & TPluralKeys extends never ? {} : {
362
+ count: number;
363
+ }, ExpandRecursive<UnionToIntersection<ReplaceFormatsFromMessage<TMessages[TKeys]>>>>>>;
364
+ /** @alpha */
365
+ interface TranslationFunction<TMessages extends {
366
+ [key in string]: string;
367
+ }> {
368
+ <TKey extends keyof CollapsedMessages<TMessages>>(key: TKey, ...[args]: TranslationFunctionOptions<NestedMessageKeys<TKey, CollapsedMessages<TMessages>>, PluralKeys<TMessages>, CollapsedMessages<TMessages>>): CollapsedMessages<TMessages>[TKey];
369
+ }
65
370
  /** @alpha */
66
- declare const useTranslationRef: <Messages extends Record<keyof Messages, string>>(translationRef: TranslationRef<Messages>) => <Tkey extends keyof Messages>(key: Tkey, options?: TranslationOptions) => Messages[Tkey];
371
+ type TranslationSnapshot<TMessages extends {
372
+ [key in string]: string;
373
+ }> = {
374
+ ready: false;
375
+ } | {
376
+ ready: true;
377
+ t: TranslationFunction<TMessages>;
378
+ };
379
+ /** @alpha */
380
+ type TranslationApi = {
381
+ getTranslation<TMessages extends {
382
+ [key in string]: string;
383
+ }>(translationRef: TranslationRef<string, TMessages>): TranslationSnapshot<TMessages>;
384
+ translation$<TMessages extends {
385
+ [key in string]: string;
386
+ }>(translationRef: TranslationRef<string, TMessages>): Observable<TranslationSnapshot<TMessages>>;
387
+ };
388
+ /**
389
+ * @alpha
390
+ */
391
+ declare const translationApiRef: ApiRef<TranslationApi>;
67
392
 
68
393
  /** @alpha */
69
- type AppTranslationApi = {
70
- getI18n(): i18n;
71
- addResourcesByRef<Messages extends Record<string, string>>(translationRef: TranslationRef<Messages>): void;
394
+ type AppLanguageApi = {
395
+ getAvailableLanguages(): {
396
+ languages: string[];
397
+ };
398
+ setLanguage(language?: string): void;
399
+ getLanguage(): {
400
+ language: string;
401
+ };
402
+ language$(): Observable<{
403
+ language: string;
404
+ }>;
72
405
  };
73
406
  /**
74
407
  * @alpha
75
408
  */
76
- declare const appTranslationApiRef: ApiRef<AppTranslationApi>;
409
+ declare const appLanguageApiRef: ApiRef<AppLanguageApi>;
410
+
411
+ /** @alpha */
412
+ declare const useTranslationRef: <TMessages extends {
413
+ [x: string]: string;
414
+ }>(translationRef: TranslationRef<string, TMessages>) => {
415
+ t: TranslationFunction<TMessages>;
416
+ };
77
417
 
78
- export { AppTranslationApi, PluginOptionsProviderProps, PluginProvider, TranslationOptions, TranslationRef, TranslationRefConfig, TranslationRefImpl, appTranslationApiRef, createTranslationRef, usePluginOptions, useTranslationRef };
418
+ export { AppLanguageApi, PluginOptionsProviderProps, PluginProvider, TranslationApi, TranslationFunction, TranslationMessages, TranslationMessagesOptions, TranslationRef, TranslationRefOptions, TranslationResource, TranslationResourceOptions, TranslationSnapshot, appLanguageApiRef, createTranslationMessages, createTranslationRef, createTranslationResource, translationApiRef, usePluginOptions, useTranslationRef };
package/dist/alpha.esm.js CHANGED
@@ -1,46 +1,179 @@
1
- import { u as useApi } from './esm/usePluginOptions-6dc02fab.esm.js';
1
+ import { u as useApi, r as errorApiRef } from './esm/usePluginOptions-6dc02fab.esm.js';
2
2
  export { P as PluginProvider, y as usePluginOptions } from './esm/usePluginOptions-6dc02fab.esm.js';
3
- import { useTranslation } from 'react-i18next';
3
+ import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
4
4
  import { createApiRef } from '@backstage/core-plugin-api';
5
- import 'react';
6
5
  import '@backstage/version-bridge';
7
6
 
7
+ function createTranslationMessages(options) {
8
+ return {
9
+ $$type: "@backstage/TranslationMessages",
10
+ id: options.ref.id,
11
+ full: Boolean(options.full),
12
+ messages: options.messages
13
+ };
14
+ }
15
+
16
+ function createTranslationResource(options) {
17
+ return {
18
+ $$type: "@backstage/TranslationResource",
19
+ version: "v1",
20
+ id: options.ref.id,
21
+ resources: Object.entries(options.translations).map(
22
+ ([language, loader]) => ({
23
+ language,
24
+ loader: () => loader().then((m) => {
25
+ const value = m.default;
26
+ return {
27
+ messages: (value == null ? void 0 : value.$$type) === "@backstage/TranslationMessages" ? value.messages : value
28
+ };
29
+ })
30
+ })
31
+ )
32
+ };
33
+ }
34
+
35
+ var __defProp = Object.defineProperty;
36
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
37
+ var __publicField = (obj, key, value) => {
38
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
39
+ return value;
40
+ };
41
+ var __accessCheck = (obj, member, msg) => {
42
+ if (!member.has(obj))
43
+ throw TypeError("Cannot " + msg);
44
+ };
45
+ var __privateGet = (obj, member, getter) => {
46
+ __accessCheck(obj, member, "read from private field");
47
+ return getter ? getter.call(obj) : member.get(obj);
48
+ };
49
+ var __privateAdd = (obj, member, value) => {
50
+ if (member.has(obj))
51
+ throw TypeError("Cannot add the same private member more than once");
52
+ member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
53
+ };
54
+ var __privateSet = (obj, member, value, setter) => {
55
+ __accessCheck(obj, member, "write to private field");
56
+ setter ? setter.call(obj, value) : member.set(obj, value);
57
+ return value;
58
+ };
59
+ var _id, _messages, _resources;
8
60
  class TranslationRefImpl {
9
- constructor(config) {
10
- this.config = config;
61
+ constructor(options) {
62
+ __privateAdd(this, _id, void 0);
63
+ __privateAdd(this, _messages, void 0);
64
+ __privateAdd(this, _resources, void 0);
65
+ __publicField(this, "$$type", "@backstage/TranslationRef");
66
+ __publicField(this, "version", "v1");
67
+ __privateSet(this, _id, options.id);
68
+ __privateSet(this, _messages, options.messages);
11
69
  }
12
- static create(config) {
13
- return new TranslationRefImpl(config);
70
+ get id() {
71
+ return __privateGet(this, _id);
14
72
  }
15
- getId() {
16
- return this.config.id;
73
+ get T() {
74
+ throw new Error("Not implemented");
17
75
  }
18
76
  getDefaultMessages() {
19
- return this.config.messages;
77
+ return __privateGet(this, _messages);
20
78
  }
21
- getLazyResources() {
22
- return this.config.lazyResources;
79
+ setDefaultResource(resources) {
80
+ __privateSet(this, _resources, resources);
23
81
  }
24
- getResources() {
25
- return this.config.resources;
82
+ getDefaultResource() {
83
+ return __privateGet(this, _resources);
26
84
  }
27
85
  toString() {
28
- return `TranslationRef(${this.getId()})`;
86
+ return `TranslationRef{id=${this.id}}`;
87
+ }
88
+ }
89
+ _id = new WeakMap();
90
+ _messages = new WeakMap();
91
+ _resources = new WeakMap();
92
+ function createTranslationRef(config) {
93
+ const ref = new TranslationRefImpl(config);
94
+ if (config.translations) {
95
+ ref.setDefaultResource(
96
+ createTranslationResource({
97
+ ref,
98
+ translations: config.translations
99
+ })
100
+ );
29
101
  }
102
+ return ref;
30
103
  }
31
- const createTranslationRef = (config) => TranslationRefImpl.create(config);
32
104
 
33
- const appTranslationApiRef = createApiRef({
34
- id: "core.apptranslation"
105
+ const translationApiRef = createApiRef({
106
+ id: "core.translation"
35
107
  });
36
108
 
109
+ const appLanguageApiRef = createApiRef({
110
+ id: "core.applanguage"
111
+ });
112
+
113
+ const loggedRefs = /* @__PURE__ */ new WeakSet();
37
114
  const useTranslationRef = (translationRef) => {
38
- const appTranslationApi = useApi(appTranslationApiRef);
39
- appTranslationApi.addResourcesByRef(translationRef);
40
- const { t } = useTranslation(translationRef.getId());
41
- const defaulteMessage = translationRef.getDefaultMessages();
42
- return (key, options) => t(key, defaulteMessage[key], options);
115
+ const errorApi = useApi(errorApiRef);
116
+ const translationApi = useApi(translationApiRef);
117
+ const [snapshot, setSnapshot] = useState(
118
+ () => translationApi.getTranslation(translationRef)
119
+ );
120
+ const observable = useMemo(
121
+ () => translationApi.translation$(translationRef),
122
+ [translationApi, translationRef]
123
+ );
124
+ const onError = useCallback(
125
+ (error) => {
126
+ if (!loggedRefs.has(translationRef)) {
127
+ const errMsg = `Failed to load translation resource '${translationRef.id}'; caused by ${error}`;
128
+ console.error(errMsg);
129
+ errorApi.post(new Error(errMsg));
130
+ loggedRefs.add(translationRef);
131
+ }
132
+ },
133
+ [errorApi, translationRef]
134
+ );
135
+ useEffect(() => {
136
+ const subscription = observable.subscribe({
137
+ next(next) {
138
+ if (next.ready) {
139
+ setSnapshot(next);
140
+ }
141
+ },
142
+ error(error) {
143
+ onError(error);
144
+ }
145
+ });
146
+ return () => {
147
+ subscription.unsubscribe();
148
+ };
149
+ }, [observable, onError]);
150
+ const initialRenderRef = useRef(true);
151
+ useEffect(() => {
152
+ if (initialRenderRef.current) {
153
+ initialRenderRef.current = false;
154
+ } else {
155
+ setSnapshot(translationApi.getTranslation(translationRef));
156
+ }
157
+ }, [translationApi, translationRef]);
158
+ if (!snapshot.ready) {
159
+ throw new Promise((resolve) => {
160
+ const subscription = observable.subscribe({
161
+ next(next) {
162
+ if (next.ready) {
163
+ subscription.unsubscribe();
164
+ resolve();
165
+ }
166
+ },
167
+ error(error) {
168
+ subscription.unsubscribe();
169
+ onError(error);
170
+ resolve();
171
+ }
172
+ });
173
+ });
174
+ }
175
+ return { t: snapshot.t };
43
176
  };
44
177
 
45
- export { TranslationRefImpl, appTranslationApiRef, createTranslationRef, useTranslationRef };
178
+ export { appLanguageApiRef, createTranslationMessages, createTranslationRef, createTranslationResource, translationApiRef, useTranslationRef };
46
179
  //# sourceMappingURL=alpha.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"alpha.esm.js","sources":["../src/translation/TranslationRef.ts","../src/apis/definitions/AppTranslationApi.ts","../src/translation/useTranslationRef.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { TranslationRef, TranslationRefConfig } from './types';\n\n/** @alpha */\nexport class TranslationRefImpl<Messages extends Record<keyof Messages, string>>\n implements TranslationRef<Messages>\n{\n static create<Messages extends Record<keyof Messages, string>>(\n config: TranslationRefConfig<Messages>,\n ) {\n return new TranslationRefImpl(config);\n }\n\n getId() {\n return this.config.id;\n }\n\n getDefaultMessages(): Messages {\n return this.config.messages;\n }\n\n getLazyResources():\n | Record<string, () => Promise<{ messages: Messages }>>\n | undefined {\n return this.config.lazyResources;\n }\n\n getResources(): Record<string, Messages> | undefined {\n return this.config.resources;\n }\n\n toString() {\n return `TranslationRef(${this.getId()})`;\n }\n\n private constructor(\n private readonly config: TranslationRefConfig<Messages>,\n ) {}\n}\n\n/** @alpha */\nexport const createTranslationRef = <\n Messages extends Record<keyof Messages, string> = {},\n>(\n config: TranslationRefConfig<Messages>,\n): TranslationRef<Messages> => TranslationRefImpl.create(config);\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { type i18n } from 'i18next';\nimport { TranslationRef } from '../../translation';\nimport { ApiRef, createApiRef } from '@backstage/core-plugin-api';\n\n/** @alpha */\nexport type AppTranslationApi = {\n getI18n(): i18n;\n\n addResourcesByRef<Messages extends Record<string, string>>(\n translationRef: TranslationRef<Messages>,\n ): void;\n};\n\n/**\n * @alpha\n */\nexport const appTranslationApiRef: ApiRef<AppTranslationApi> = createApiRef({\n id: 'core.apptranslation',\n});\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { useTranslation } from 'react-i18next';\n\nimport { TranslationOptions, TranslationRef } from './types';\nimport { useApi } from '../apis';\nimport { appTranslationApiRef } from '../apis/alpha';\n\n/** @alpha */\nexport const useTranslationRef = <\n Messages extends Record<keyof Messages, string>,\n>(\n translationRef: TranslationRef<Messages>,\n) => {\n const appTranslationApi = useApi(appTranslationApiRef);\n\n appTranslationApi.addResourcesByRef(translationRef);\n\n const { t } = useTranslation(translationRef.getId());\n\n const defaulteMessage = translationRef.getDefaultMessages();\n\n return <Tkey extends keyof Messages>(\n key: Tkey,\n options?: TranslationOptions,\n ): Messages[Tkey] => t(key as string, defaulteMessage[key], options);\n};\n"],"names":[],"mappings":";;;;;;;AAmBO,MAAM,kBAEb,CAAA;AAAA,EA6BU,YACW,MACjB,EAAA;AADiB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA,CAAA;AAAA,GAChB;AAAA,EA9BH,OAAO,OACL,MACA,EAAA;AACA,IAAO,OAAA,IAAI,mBAAmB,MAAM,CAAA,CAAA;AAAA,GACtC;AAAA,EAEA,KAAQ,GAAA;AACN,IAAA,OAAO,KAAK,MAAO,CAAA,EAAA,CAAA;AAAA,GACrB;AAAA,EAEA,kBAA+B,GAAA;AAC7B,IAAA,OAAO,KAAK,MAAO,CAAA,QAAA,CAAA;AAAA,GACrB;AAAA,EAEA,gBAEc,GAAA;AACZ,IAAA,OAAO,KAAK,MAAO,CAAA,aAAA,CAAA;AAAA,GACrB;AAAA,EAEA,YAAqD,GAAA;AACnD,IAAA,OAAO,KAAK,MAAO,CAAA,SAAA,CAAA;AAAA,GACrB;AAAA,EAEA,QAAW,GAAA;AACT,IAAO,OAAA,CAAA,eAAA,EAAkB,IAAK,CAAA,KAAA,EAAO,CAAA,CAAA,CAAA,CAAA;AAAA,GACvC;AAKF,CAAA;AAGO,MAAM,oBAAuB,GAAA,CAGlC,MAC6B,KAAA,kBAAA,CAAmB,OAAO,MAAM;;AC5BxD,MAAM,uBAAkD,YAAa,CAAA;AAAA,EAC1E,EAAI,EAAA,qBAAA;AACN,CAAC;;ACXY,MAAA,iBAAA,GAAoB,CAG/B,cACG,KAAA;AACH,EAAM,MAAA,iBAAA,GAAoB,OAAO,oBAAoB,CAAA,CAAA;AAErD,EAAA,iBAAA,CAAkB,kBAAkB,cAAc,CAAA,CAAA;AAElD,EAAA,MAAM,EAAE,CAAE,EAAA,GAAI,cAAe,CAAA,cAAA,CAAe,OAAO,CAAA,CAAA;AAEnD,EAAM,MAAA,eAAA,GAAkB,eAAe,kBAAmB,EAAA,CAAA;AAE1D,EAAO,OAAA,CACL,KACA,OACmB,KAAA,CAAA,CAAE,KAAe,eAAgB,CAAA,GAAG,GAAG,OAAO,CAAA,CAAA;AACrE;;;;"}
1
+ {"version":3,"file":"alpha.esm.js","sources":["../src/translation/TranslationMessages.ts","../src/translation/TranslationResource.ts","../src/translation/TranslationRef.ts","../src/apis/definitions/TranslationApi.ts","../src/apis/definitions/AppLanguageApi.ts","../src/translation/useTranslationRef.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { TranslationRef } from '@backstage/core-plugin-api/alpha';\n\n/**\n * Represents a collection of messages to be provided for a given translation ref.\n *\n * @alpha\n * @remarks\n *\n * This collection of messages can either be used directly as an override for the\n * default messages, or it can be used to provide translations for a language by\n * by being referenced by a {@link TranslationResource}.\n */\nexport interface TranslationMessages<\n TId extends string = string,\n TMessages extends { [key in string]: string } = { [key in string]: string },\n TFull extends boolean = boolean,\n> {\n $$type: '@backstage/TranslationMessages';\n /** The ID of the translation ref that these messages are for */\n id: TId;\n /** Whether or not these messages override all known messages */\n full: TFull;\n /** The messages provided for the given translation ref */\n messages: TMessages;\n}\n\n/**\n * Options for {@link createTranslationMessages}.\n *\n * @alpha\n */\nexport interface TranslationMessagesOptions<\n TId extends string,\n TMessages extends { [key in string]: string },\n TFull extends boolean,\n> {\n ref: TranslationRef<TId, TMessages>;\n\n full?: TFull;\n\n messages: false extends TFull\n ? { [key in keyof TMessages]?: string | null }\n : { [key in keyof TMessages]: string | null };\n}\n\n/**\n * Creates a collection of messages for a given translation ref.\n *\n * @alpha\n */\nexport function createTranslationMessages<\n TId extends string,\n TMessages extends { [key in string]: string },\n TFull extends boolean,\n>(\n options: TranslationMessagesOptions<TId, TMessages, TFull>,\n): TranslationMessages<TId, TMessages, TFull> {\n return {\n $$type: '@backstage/TranslationMessages',\n id: options.ref.id,\n full: Boolean(options.full) as TFull,\n messages: options.messages as TMessages,\n };\n}\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n TranslationMessages,\n TranslationRef,\n} from '@backstage/core-plugin-api/alpha';\n\n/** @alpha */\nexport interface TranslationResource<TId extends string = string> {\n $$type: '@backstage/TranslationResource';\n id: TId;\n}\n\n/** @internal */\nexport type InternalTranslationResourceLoader = () => Promise<{\n messages: { [key in string]: string | null };\n}>;\n\n/** @internal */\nexport interface InternalTranslationResource<TId extends string = string>\n extends TranslationResource<TId> {\n version: 'v1';\n resources: Array<{\n language: string;\n loader: InternalTranslationResourceLoader;\n }>;\n}\n\n/** @internal */\nexport function toInternalTranslationResource<TId extends string>(\n resource: TranslationResource<TId>,\n): InternalTranslationResource<TId> {\n const r = resource as InternalTranslationResource<TId>;\n if (r.$$type !== '@backstage/TranslationResource') {\n throw new Error(`Invalid translation resource, bad type '${r.$$type}'`);\n }\n if (r.version !== 'v1') {\n throw new Error(`Invalid translation resource, bad version '${r.version}'`);\n }\n\n return r;\n}\n\n/** @alpha */\nexport interface TranslationResourceOptions<\n TId extends string,\n TMessages extends { [key in string]: string },\n TTranslations extends {\n [language in string]: () => Promise<{\n default:\n | TranslationMessages<TId>\n | { [key in keyof TMessages]: string | null };\n }>;\n },\n> {\n ref: TranslationRef<TId, TMessages>;\n\n translations: TTranslations;\n}\n\n/** @alpha */\nexport function createTranslationResource<\n TId extends string,\n TMessages extends { [key in string]: string },\n TTranslations extends {\n [language in string]: () => Promise<{\n default:\n | TranslationMessages<TId>\n | { [key in keyof TMessages]: string | null };\n }>;\n },\n>(\n options: TranslationResourceOptions<TId, TMessages, TTranslations>,\n): TranslationResource<TId> {\n return {\n $$type: '@backstage/TranslationResource',\n version: 'v1',\n id: options.ref.id,\n resources: Object.entries(options.translations).map(\n ([language, loader]) => ({\n language,\n loader: () =>\n loader().then(m => {\n const value = m.default;\n return {\n messages:\n value?.$$type === '@backstage/TranslationMessages'\n ? value.messages\n : value,\n };\n }),\n }),\n ),\n } as InternalTranslationResource<TId>;\n}\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n createTranslationResource,\n TranslationResource,\n} from './TranslationResource';\n\n/** @alpha */\nexport interface TranslationRef<\n TId extends string = string,\n TMessages extends { [key in string]: string } = { [key in string]: string },\n> {\n $$type: '@backstage/TranslationRef';\n\n id: TId;\n\n T: TMessages;\n}\n\n/** @internal */\ntype AnyMessages = { [key in string]: string };\n\n/** @internal */\nexport interface InternalTranslationRef<\n TId extends string = string,\n TMessages extends { [key in string]: string } = { [key in string]: string },\n> extends TranslationRef<TId, TMessages> {\n version: 'v1';\n\n getDefaultMessages(): AnyMessages;\n\n getDefaultResource(): TranslationResource | undefined;\n}\n\n/** @alpha */\nexport interface TranslationRefOptions<\n TId extends string,\n TMessages extends { [key in string]: string },\n TTranslations extends {\n [language in string]: () => Promise<{\n default: { [key in keyof TMessages]: string | null };\n }>;\n },\n> {\n id: TId;\n messages: TMessages;\n translations?: TTranslations;\n}\n\n/** @internal */\nclass TranslationRefImpl<\n TId extends string,\n TMessages extends { [key in string]: string },\n> implements InternalTranslationRef<TId, TMessages>\n{\n #id: TId;\n #messages: TMessages;\n #resources: TranslationResource | undefined;\n\n constructor(options: TranslationRefOptions<TId, TMessages, any>) {\n this.#id = options.id;\n this.#messages = options.messages;\n }\n\n $$type = '@backstage/TranslationRef' as const;\n\n version = 'v1' as const;\n\n get id(): TId {\n return this.#id;\n }\n\n get T(): never {\n throw new Error('Not implemented');\n }\n\n getDefaultMessages(): AnyMessages {\n return this.#messages;\n }\n\n setDefaultResource(resources: TranslationResource): void {\n this.#resources = resources;\n }\n\n getDefaultResource(): TranslationResource | undefined {\n return this.#resources;\n }\n\n toString() {\n return `TranslationRef{id=${this.id}}`;\n }\n}\n\n/** @alpha */\nexport function createTranslationRef<\n TId extends string,\n const TMessages extends { [key in string]: string },\n TTranslations extends {\n [language in string]: () => Promise<{\n default: { [key in keyof TMessages]: string | null };\n }>;\n },\n>(\n config: TranslationRefOptions<TId, TMessages, TTranslations>,\n): TranslationRef<TId, TMessages> {\n const ref = new TranslationRefImpl(config);\n if (config.translations) {\n ref.setDefaultResource(\n createTranslationResource({\n ref,\n translations: config.translations as any,\n }),\n );\n }\n return ref;\n}\n\n/** @internal */\nexport function toInternalTranslationRef<\n TId extends string,\n TMessages extends { [key in string]: string },\n>(ref: TranslationRef<TId, TMessages>): InternalTranslationRef<TId, TMessages> {\n const r = ref as InternalTranslationRef<TId, TMessages>;\n if (r.$$type !== '@backstage/TranslationRef') {\n throw new Error(`Invalid translation ref, bad type '${r.$$type}'`);\n }\n if (r.version !== 'v1') {\n throw new Error(`Invalid translation ref, bad version '${r.version}'`);\n }\n return r;\n}\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ApiRef, createApiRef } from '@backstage/core-plugin-api';\nimport { Observable } from '@backstage/types';\nimport { TranslationRef } from '../../translation';\n\n/**\n * Base translation options.\n *\n * @alpha\n */\ninterface BaseOptions {\n interpolation?: {\n /** Whether to HTML escape provided values, defaults to false */\n escapeValue?: boolean;\n };\n}\n\n/**\n * All pluralization suffixes supported by i18next\n *\n * @ignore\n */\ntype TranslationPlural = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\n/**\n * A mapping of i18n formatting types to their corresponding types and options.\n * @ignore\n */\ntype I18nextFormatMap = {\n number: {\n type: number;\n options: Intl.NumberFormatOptions;\n };\n currency: {\n type: number;\n options: Intl.NumberFormatOptions;\n };\n datetime: {\n type: Date;\n options: Intl.DateTimeFormatOptions;\n };\n relativetime: {\n type: number;\n options: {\n range?: Intl.RelativeTimeFormatUnit;\n } & Intl.RelativeTimeFormatOptions;\n };\n list: {\n type: string[];\n options: Intl.ListFormatOptions;\n };\n};\n\n/**\n * Extracts all pluralized keys from the message map.\n *\n * @example\n * ```\n * { foo: 'foo', bar_one: 'bar', bar_other: 'bars' } -> 'bar'\n * ```\n *\n * @ignore\n */\ntype PluralKeys<TMessages extends { [key in string]: string }> = {\n [Key in keyof TMessages]: Key extends `${infer K}_${TranslationPlural}`\n ? K\n : never;\n}[keyof TMessages];\n\n/**\n * Collapses a message map into normalized keys with union values.\n *\n * @example\n * ```\n * { foo_one: 'foo', foo_other: 'foos' } -> { foo: 'foo' | 'foos' }\n * ```\n *\n * @ignore\n */\ntype CollapsedMessages<TMessages extends { [key in string]: string }> = {\n [key in keyof TMessages as key extends `${infer K}_${TranslationPlural}`\n ? K\n : key]: TMessages[key];\n};\n\n/**\n * Helper type that expands type hints\n *\n * @ignore\n */\ntype Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;\n\n/**\n * Helper type that expands type hints recursively\n *\n * @ignore\n */\ntype ExpandRecursive<T> = T extends infer O\n ? { [K in keyof O]: ExpandRecursive<O[K]> }\n : never;\n\n/**\n * Trim away whitespace\n *\n * @ignore\n */\ntype Trim<T> = T extends ` ${infer U}`\n ? Trim<U>\n : T extends `${infer U} `\n ? Trim<U>\n : T;\n\n/**\n * Extracts the key and format from a replacement string.\n *\n * @example\n * ```\n * 'foo, number' -> { foo: number }, 'foo' -> { foo: undefined }\n * ```\n */\ntype ExtractFormat<Replacement extends string> =\n Replacement extends `${infer Key},${infer FullFormat}`\n ? {\n [key in Trim<Key>]: Lowercase<\n Trim<\n FullFormat extends `${infer Format}(${string})${string}`\n ? Format\n : FullFormat\n >\n >;\n }\n : { [key in Trim<Replacement>]: undefined };\n\n/**\n * Expand the keys in a flat map to nested objects.\n *\n * @example\n * ```\n * { 'a.b': 'foo', 'a.c': 'bar' } -> { a: { b: 'foo', c: 'bar' }\n * ```\n *\n * @ignore\n */\ntype ExpandKeys<TMap extends {}> = {\n [Key in keyof TMap as Key extends `${infer Prefix}.${string}`\n ? Prefix\n : Key]: Key extends `${string}.${infer Rest}`\n ? ExpandKeys<{ [key in Rest]: TMap[Key] }>\n : TMap[Key];\n};\n\n/**\n * Extracts all option keys and their format from a message string.\n *\n * @example\n * ```\n * 'foo {{bar}} {{baz, number}}' -> { 'bar': undefined, 'baz': 'number' }\n * ```\n *\n * @ignore\n */\ntype ReplaceFormatsFromMessage<TMessage> =\n TMessage extends `${string}{{${infer Replacement}}}${infer Tail}` // no formatting, e.g. {{foo}}\n ? ExpandKeys<ExtractFormat<Replacement>> & ReplaceFormatsFromMessage<Tail>\n : {};\n\n/**\n * Generates the replace options structure\n *\n * @ignore\n */\ntype ReplaceOptionsFromFormats<TFormats extends {}> = {\n [Key in keyof TFormats]: TFormats[Key] extends keyof I18nextFormatMap\n ? I18nextFormatMap[TFormats[Key]]['type']\n : TFormats[Key] extends {}\n ? Expand<ReplaceOptionsFromFormats<TFormats[Key]>>\n : string;\n};\n\n/**\n * Generates the formatParams options structure\n *\n * @ignore\n */\ntype ReplaceFormatParamsFromFormats<TFormats extends {}> = {\n [Key in keyof TFormats]?: TFormats[Key] extends keyof I18nextFormatMap\n ? I18nextFormatMap[TFormats[Key]]['options']\n : TFormats[Key] extends {}\n ? Expand<ReplaceFormatParamsFromFormats<TFormats[Key]>>\n : undefined;\n};\n\n/**\n * Extracts all nesting keys from a message string.\n *\n * @example\n * ```\n * 'foo $t(bar) $t(baz)' -> 'bar' | 'baz'\n * ```\n *\n * @ignore\n */\ntype NestingKeysFromMessage<TMessage extends string> =\n TMessage extends `${string}$t(${infer Key})${infer Tail}` // nesting options are not supported\n ? Trim<Key> | NestingKeysFromMessage<Tail>\n : never;\n\n/**\n * Find all referenced keys, given a starting key and the full set of messages.\n *\n * This will only discover keys up to 3 levels deep.\n *\n * @example\n * ```\n * <'x', { x: '$t(y) $t(z)', y: 'y', z: '$t(w)', w: 'w', foo: 'foo' }> -> 'x' | 'y' | 'z' | 'w'\n * ```\n *\n * @ignore\n */\ntype NestedMessageKeys<\n TKey extends keyof TMessages,\n TMessages extends { [key in string]: string },\n> =\n | TKey\n | NestedMessageKeys2<NestingKeysFromMessage<TMessages[TKey]>, TMessages>;\n// Can't recursively reference ourself, so instead we got this beauty\ntype NestedMessageKeys2<\n TKey extends keyof TMessages,\n TMessages extends { [key in string]: string },\n> =\n | TKey\n | NestedMessageKeys3<NestingKeysFromMessage<TMessages[TKey]>, TMessages>;\n// Only support 3 levels of nesting\ntype NestedMessageKeys3<\n TKey extends keyof TMessages,\n TMessages extends { [key in string]: string },\n> = TKey | NestingKeysFromMessage<TMessages[TKey]>;\n\n/**\n * Converts a union type to an intersection type.\n *\n * @example\n * ```\n * { foo: 'foo' } | { bar: 'bar' } -> { foo: 'foo' } & { bar: 'bar' }\n * ```\n *\n * @ignore\n */\ntype UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (\n k: infer I,\n) => void\n ? I\n : never;\n\n/**\n * Collects different types of options into a single object\n *\n * @ignore\n */\ntype CollectOptions<\n TCount extends { count?: number },\n TFormats extends {},\n> = TCount &\n // count is special, omit it from the replacements\n (keyof Omit<TFormats, 'count'> extends never\n ? {}\n : (\n | Expand<Omit<ReplaceOptionsFromFormats<TFormats>, 'count'>>\n | {\n replace: Expand<Omit<ReplaceOptionsFromFormats<TFormats>, 'count'>>;\n }\n ) & {\n formatParams?: Expand<ReplaceFormatParamsFromFormats<TFormats>>;\n });\n\n/**\n * Helper type to only require options argument if needed\n *\n * @ignore\n */\ntype OptionArgs<TOptions extends {}> = keyof TOptions extends never\n ? [options?: BaseOptions]\n : [options: BaseOptions & TOptions];\n\n/**\n * @ignore\n */\ntype TranslationFunctionOptions<\n TKeys extends keyof TMessages, // All normalized message keys to be considered, i.e. included nested ones\n TPluralKeys extends keyof TMessages, // All keys in the message map that are pluralized\n TMessages extends { [key in string]: string }, // Collapsed message map with normalized keys and union values\n> = OptionArgs<\n Expand<\n CollectOptions<\n TKeys & TPluralKeys extends never ? {} : { count: number },\n ExpandRecursive<\n UnionToIntersection<ReplaceFormatsFromMessage<TMessages[TKeys]>>\n >\n >\n >\n>;\n\n/** @alpha */\nexport interface TranslationFunction<\n TMessages extends { [key in string]: string },\n> {\n <TKey extends keyof CollapsedMessages<TMessages>>(\n key: TKey,\n ...[args]: TranslationFunctionOptions<\n NestedMessageKeys<TKey, CollapsedMessages<TMessages>>,\n PluralKeys<TMessages>,\n CollapsedMessages<TMessages>\n >\n ): CollapsedMessages<TMessages>[TKey];\n}\n\n/** @alpha */\nexport type TranslationSnapshot<TMessages extends { [key in string]: string }> =\n { ready: false } | { ready: true; t: TranslationFunction<TMessages> };\n\n/** @alpha */\nexport type TranslationApi = {\n getTranslation<TMessages extends { [key in string]: string }>(\n translationRef: TranslationRef<string, TMessages>,\n ): TranslationSnapshot<TMessages>;\n\n translation$<TMessages extends { [key in string]: string }>(\n translationRef: TranslationRef<string, TMessages>,\n ): Observable<TranslationSnapshot<TMessages>>;\n};\n\n/**\n * @alpha\n */\nexport const translationApiRef: ApiRef<TranslationApi> = createApiRef({\n id: 'core.translation',\n});\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ApiRef, createApiRef } from '@backstage/core-plugin-api';\nimport { Observable } from '@backstage/types';\n\n/** @alpha */\nexport type AppLanguageApi = {\n getAvailableLanguages(): { languages: string[] };\n\n setLanguage(language?: string): void;\n\n getLanguage(): { language: string };\n\n language$(): Observable<{ language: string }>;\n};\n\n/**\n * @alpha\n */\nexport const appLanguageApiRef: ApiRef<AppLanguageApi> = createApiRef({\n id: 'core.applanguage',\n});\n","/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { errorApiRef, useApi } from '../apis';\nimport {\n translationApiRef,\n TranslationFunction,\n TranslationSnapshot,\n} from '../apis/alpha';\nimport { TranslationRef } from './TranslationRef';\n\n// Make sure we don't fill the logs with loading errors for the same ref\nconst loggedRefs = new WeakSet<TranslationRef<string, {}>>();\n\n/** @alpha */\nexport const useTranslationRef = <\n TMessages extends { [key in string]: string },\n>(\n translationRef: TranslationRef<string, TMessages>,\n): { t: TranslationFunction<TMessages> } => {\n const errorApi = useApi(errorApiRef);\n const translationApi = useApi(translationApiRef);\n\n const [snapshot, setSnapshot] = useState<TranslationSnapshot<TMessages>>(() =>\n translationApi.getTranslation(translationRef),\n );\n const observable = useMemo(\n () => translationApi.translation$(translationRef),\n [translationApi, translationRef],\n );\n\n const onError = useCallback(\n (error: Error) => {\n if (!loggedRefs.has(translationRef)) {\n const errMsg = `Failed to load translation resource '${translationRef.id}'; caused by ${error}`;\n // eslint-disable-next-line no-console\n console.error(errMsg);\n errorApi.post(new Error(errMsg));\n loggedRefs.add(translationRef);\n }\n },\n [errorApi, translationRef],\n );\n\n useEffect(() => {\n const subscription = observable.subscribe({\n next(next) {\n if (next.ready) {\n setSnapshot(next);\n }\n },\n error(error) {\n onError(error);\n },\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, [observable, onError]);\n\n // Keep track of if the provided translation ref changes, and in that case update the snapshot\n const initialRenderRef = useRef(true);\n useEffect(() => {\n if (initialRenderRef.current) {\n initialRenderRef.current = false;\n } else {\n setSnapshot(translationApi.getTranslation(translationRef));\n }\n }, [translationApi, translationRef]);\n\n if (!snapshot.ready) {\n throw new Promise<void>(resolve => {\n const subscription = observable.subscribe({\n next(next) {\n if (next.ready) {\n subscription.unsubscribe();\n resolve();\n }\n },\n error(error) {\n subscription.unsubscribe();\n onError(error);\n resolve();\n },\n });\n });\n }\n\n return { t: snapshot.t };\n};\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@backstage/core-plugin-api",
3
3
  "description": "Core API used by Backstage plugins",
4
- "version": "1.6.0-next.3",
4
+ "version": "1.6.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -44,14 +44,13 @@
44
44
  "start": "backstage-cli package start"
45
45
  },
46
46
  "dependencies": {
47
- "@backstage/config": "^1.1.0-next.2",
48
- "@backstage/types": "^1.1.1-next.0",
49
- "@backstage/version-bridge": "^1.0.5-next.0",
47
+ "@backstage/config": "^1.1.0",
48
+ "@backstage/types": "^1.1.1",
49
+ "@backstage/version-bridge": "^1.0.5",
50
50
  "@types/react": "^16.13.1 || ^17.0.0",
51
51
  "history": "^5.0.0",
52
52
  "i18next": "^22.4.15",
53
53
  "prop-types": "^15.7.2",
54
- "react-i18next": "^12.3.1",
55
54
  "zen-observable": "^0.10.0"
56
55
  },
57
56
  "peerDependencies": {
@@ -60,9 +59,9 @@
60
59
  "react-router-dom": "6.0.0-beta.0 || ^6.3.0"
61
60
  },
62
61
  "devDependencies": {
63
- "@backstage/cli": "^0.22.13-next.3",
64
- "@backstage/core-app-api": "^1.10.0-next.3",
65
- "@backstage/test-utils": "^1.4.3-next.3",
62
+ "@backstage/cli": "^0.22.13",
63
+ "@backstage/core-app-api": "^1.10.0",
64
+ "@backstage/test-utils": "^1.4.3",
66
65
  "@testing-library/dom": "^8.0.0",
67
66
  "@testing-library/jest-dom": "^5.10.1",
68
67
  "@testing-library/react": "^12.1.3",