@lvl99dotcom/paintcanvas 0.2.0 → 0.3.1

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.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/dist/lib/classes/PaintCanvas/index.js +2 -1
  3. package/dist/lib/i18n/index.d.ts +50 -13
  4. package/dist/lib/i18n/index.js +121 -26
  5. package/dist/locales/en-GB.js +177 -184
  6. package/dist/locales/en-US.js +177 -184
  7. package/dist/locales/fr.js +177 -184
  8. package/dist/locales/ja.js +177 -184
  9. package/dist/locales/ko.js +177 -184
  10. package/dist/locales/zh-Hans.js +177 -184
  11. package/dist/locales/zh-Hant.js +177 -184
  12. package/dist/package.js +1 -1
  13. package/dist/ui/base/index.d.ts +1 -1
  14. package/dist/ui/base/index.js +1 -1
  15. package/dist/ui/elements/PaintCanvas/index.js +20 -11
  16. package/dist/ui/elements/PcBrushConfig/index.d.ts +3 -3
  17. package/dist/ui/elements/PcBrushConfig/index.js +44 -41
  18. package/dist/ui/elements/PcBrushMenu/index.js +20 -19
  19. package/dist/ui/elements/PcBrushTest/index.js +2 -2
  20. package/dist/ui/elements/PcButton/index.js +1 -0
  21. package/dist/ui/elements/PcCanvas/index.js +1 -1
  22. package/dist/ui/elements/PcCanvasMap/index.js +1 -1
  23. package/dist/ui/elements/PcDialog/index.js +1 -0
  24. package/dist/ui/elements/PcIconToggle/index.js +1 -0
  25. package/dist/ui/elements/PcInfoTip/index.js +1 -1
  26. package/dist/ui/elements/PcInputSettings/index.js +17 -13
  27. package/dist/ui/elements/PcLayerItem/index.js +34 -34
  28. package/dist/ui/elements/PcLayersMenu/index.js +1 -1
  29. package/dist/ui/elements/PcStats/index.js +8 -5
  30. package/dist/ui/elements/PcThemePicker/index.js +3 -2
  31. package/dist/ui/elements/PcToast/index.js +2 -1
  32. package/dist/ui/elements/PcToolSettings/index.js +51 -45
  33. package/dist/ui/elements/PcToolbar/index.js +19 -19
  34. package/locales/en-GB.json +177 -184
  35. package/locales/en-US.json +177 -184
  36. package/locales/fr.json +177 -184
  37. package/locales/ja.json +177 -184
  38. package/locales/ko.json +177 -184
  39. package/locales/zh-Hans.json +177 -184
  40. package/locales/zh-Hant.json +177 -184
  41. package/package.json +1 -1
package/README.md CHANGED
@@ -75,7 +75,7 @@ Guides and API pages are in [docs/](docs/), a [Docusaurus](https://docusaurus.io
75
75
  - [Building your own UI on the engine](docs/docs/engine.md)
76
76
  - [Messages and events](docs/docs/messages.md)
77
77
  - [Saving and opening drawings](docs/docs/documents.md)
78
- - [Translations](docs/docs/translations.md)
78
+ - [Translations, and adding your app's own](docs/docs/translations.md)
79
79
  - [Icons](docs/docs/icons.md)
80
80
  - [Theming](docs/docs/theming.md)
81
81
 
@@ -92,7 +92,7 @@ Starts a development server and opens the demo in your browser. It uses the sour
92
92
 
93
93
  To try it with a pen or touch on another device (e.g. a tablet) on the same network, run `pnpm dev --host` and open the network address it prints. This makes the server reachable by anyone on that network.
94
94
 
95
- The UI text follows your device's languages: on an English (UK) device, edit `locales/en-GB.json` to see text changes.
95
+ The UI text follows your device's languages: on an English (UK) device, edit `locales/en-GB.json` to see text changes. The demo's own text (e.g. the "New drawing" button) is in `demo/locales/`, added the way an app adds its translations.
96
96
 
97
97
  ## Development
98
98
 
@@ -13,8 +13,9 @@ import Layer, { createOffscreenCanvas } from "../Layer/index.js";
13
13
  import LayerAutosave from "../LayerAutosave/index.js";
14
14
  import { brushPresets, eraserPresets } from "../../brushes/index.js";
15
15
  import { BUILT_IN_AUTHOR, UNKNOWN_AUTHOR, authorHandle } from "../../brushes/authors/index.js";
16
- import { BRUSH_PRESET_FILE_FORMAT } from "../../presets/schema.js";
16
+ import "../../presets/index.js";
17
17
  import CustomPresets from "../CustomPresets/index.js";
18
+ import { BRUSH_PRESET_FILE_FORMAT } from "../../presets/schema.js";
18
19
  import InputSettings from "../InputSettings/index.js";
19
20
  import ExternalSheets from "../ExternalSheets/index.js";
20
21
  import PatchSender from "../../messages/PatchSender/index.js";
@@ -26,6 +26,16 @@ export declare const matchLocale: (requested: readonly string[], available: read
26
26
  * interpolate("Delete \"{name}\"?", { name: "Sketch" }); // 'Delete "Sketch"?'
27
27
  */
28
28
  export declare const interpolate: (text: string, values?: Record<string, string | number>) => string;
29
+ /**
30
+ * A locale's language name, written in that language (so people can find their own), with its first letter in
31
+ * capitals. Gives the tag itself if the browser can't name it.
32
+ *
33
+ * @example
34
+ * languageName("ja"); // "日本語"
35
+ * languageName("fr"); // "Français"
36
+ * languageName("en-GB"); // "English (United Kingdom)"
37
+ */
38
+ export declare const languageName: (locale: string) => string;
29
39
  /** Options for translations */
30
40
  export interface TranslationsOptions {
31
41
  /** Locales that can be used, and how to load each (default: all the locales that come with PaintCanvas) */
@@ -40,53 +50,80 @@ export interface TranslationsOptions {
40
50
  * match for the device, and `setLocale` to any available locale. Locale files load when first used, without reloading
41
51
  * the page. Strings missing from a locale come from the fallback locale, then the key itself.
42
52
  *
53
+ * Other projects can add their own strings and locales with `addLocales` and `addStrings`. A locale's strings from
54
+ * every set load together and are merged in the order the sets were added (PaintCanvas's first), so a key in a later
55
+ * set replaces the same key in an earlier one. Keys are prefixed by project by convention: `paintCanvas.` for
56
+ * PaintCanvas's, and e.g. `liveCanvas.` for an app's.
57
+ *
43
58
  * Dispatches a `change` event when the locale or its strings change, so the UI can show the new text.
44
59
  *
45
60
  * @example
46
61
  * const translations = new Translations({ locales: { "en-US": ..., ja: () => import("./ja.json") } });
47
62
  * await translations.useDeviceLocale();
48
- * translations.t("layers.delete", { name: "Sketch" });
63
+ * translations.t("paintCanvas.layers.delete", { name: "Sketch" });
49
64
  */
50
65
  export default class Translations extends EventTarget {
51
- private readonly loaders;
52
66
  private readonly fallback;
53
- /** Strings of each locale loaded so far, plus any added */
54
- private readonly loaded;
67
+ /** Sets of strings, in the order they were added */
68
+ private readonly sources;
69
+ /** Each locale's strings from every set loaded so far, merged in order */
70
+ private readonly merged;
55
71
  private current;
56
72
  /** The locale most recently asked for, so a slow load can't override a later choice */
57
73
  private requested?;
58
74
  constructor(options?: TranslationsOptions);
59
75
  /** The locale in use (an IETF BCP 47 tag, e.g. `ja`) */
60
76
  get locale(): string;
61
- /** Locales that can be used */
77
+ /** Locales that can be used: the fallback, and every locale a set of strings can load */
62
78
  get locales(): string[];
63
79
  /**
64
80
  * Switch to the best available locale for the device's languages (`navigator.languages`).
65
81
  */
66
82
  useDeviceLocale(languages?: readonly string[]): Promise<void>;
67
83
  /**
68
- * Switch to a locale (the best available match for the tag), loading its strings if needed. Dispatches `change` once
69
- * its strings are ready. If loading fails, the current locale stays and this rejects.
84
+ * Switch to a locale (the best available match for the tag), loading its strings from every set if needed.
85
+ * Dispatches `change` once they're ready. If loading fails, the current locale stays and this rejects.
70
86
  *
71
87
  * @example
72
88
  * await translations.setLocale("zh-TW"); // uses zh-Hant
73
89
  */
74
90
  setLocale(tag: string): Promise<void>;
75
91
  /**
76
- * Add or replace strings for a locale (e.g. the implementer's own wording or extra keys). Dispatches `change` if it's
77
- * the locale in use or the fallback.
92
+ * Add a set of strings for some locales, loaded when first used, after the sets already added (so its keys replace
93
+ * theirs). Its locales become available. The locale in use and the fallback load straight away, then `change` is
94
+ * dispatched if the set has strings for either.
78
95
  *
79
96
  * @example
80
- * translations.addStrings("en-US", { "toolbar.brush": "Paintbrush" });
97
+ * await translations.addLocales({
98
+ * "en-US": () => import("./locales/en-US.json"),
99
+ * fr: () => import("./locales/fr.json"),
100
+ * });
101
+ * translations.t("liveCanvas.share");
102
+ */
103
+ addLocales(loaders: Record<string, LocaleLoader>): Promise<void>;
104
+ /**
105
+ * Add or replace strings for a locale (e.g. the implementer's own wording or extra keys), after the sets already
106
+ * added. Dispatches `change` if it's the locale in use or the fallback.
107
+ *
108
+ * @example
109
+ * translations.addStrings("en-US", { "paintCanvas.tool.brush": "Paintbrush" });
81
110
  */
82
111
  addStrings(locale: string, strings: Strings): void;
112
+ /**
113
+ * Load a locale's strings from every set, including sets added while loading, then merge them.
114
+ */
115
+ private load;
116
+ /**
117
+ * Merge a locale's strings loaded so far from every set, in the order the sets were added.
118
+ */
119
+ private merge;
83
120
  /**
84
121
  * Translate a key, putting values into its `{name}` placeholders. With a numeric `count`, the key's plural form for
85
- * the locale is used when there is one (e.g. `presets.imported.one` and `presets.imported.other`). Falls back to the
86
- * fallback locale, then the key.
122
+ * the locale is used when there is one (e.g. `paintCanvas.presets.imported.one` and
123
+ * `paintCanvas.presets.imported.other`). Falls back to the fallback locale, then the key.
87
124
  *
88
125
  * @example
89
- * translations.t("presets.imported", { count: 3 }); // "Imported 3 brush presets"
126
+ * translations.t("paintCanvas.presets.imported", { count: 3 }); // "Imported 3 brush presets"
90
127
  */
91
128
  t(key: string, values?: Record<string, string | number>): string;
92
129
  }
@@ -82,39 +82,102 @@ var matchLocale = (requested, available, fallback = FALLBACK_LOCALE) => {
82
82
  */
83
83
  var interpolate = (text, values = {}) => text.replace(/\{(\w+)\}/g, (placeholder, name) => Object.hasOwn(values, name) ? String(values[name]) : placeholder);
84
84
  /**
85
+ * A locale's language name, written in that language (so people can find their own), with its first letter in
86
+ * capitals. Gives the tag itself if the browser can't name it.
87
+ *
88
+ * @example
89
+ * languageName("ja"); // "日本語"
90
+ * languageName("fr"); // "Français"
91
+ * languageName("en-GB"); // "English (United Kingdom)"
92
+ */
93
+ var languageName = (locale) => {
94
+ try {
95
+ const name = new Intl.DisplayNames([locale], {
96
+ type: "language",
97
+ languageDisplay: "standard",
98
+ fallback: "none"
99
+ }).of(locale);
100
+ return name ? name.charAt(0).toLocaleUpperCase(locale) + name.slice(1) : locale;
101
+ } catch {
102
+ return locale;
103
+ }
104
+ };
105
+ /** A locale loader's strings, whether given directly or as a module's default export */
106
+ var unwrap = (module) => "default" in module && typeof module.default === "object" ? module.default : module;
107
+ /**
108
+ * One set of strings for some locales (e.g. PaintCanvas's, or an app's), each loaded once, when first needed.
109
+ */
110
+ var LocaleSource = class {
111
+ loaders;
112
+ /** Strings of each locale loaded so far */
113
+ strings = /* @__PURE__ */ new Map();
114
+ /** Loads started, so a locale loading for two callers loads once */
115
+ loading = /* @__PURE__ */ new Map();
116
+ constructor(loaders = {}) {
117
+ this.loaders = loaders;
118
+ }
119
+ /**
120
+ * Load a locale's strings, if this has a loader for it and they aren't loaded yet. A failed load rejects, and can be
121
+ * tried again.
122
+ */
123
+ load(locale) {
124
+ const loader = this.loaders[locale];
125
+ if (!loader || this.strings.has(locale)) return Promise.resolve();
126
+ let loading = this.loading.get(locale);
127
+ if (!loading) {
128
+ loading = loader().then((module) => {
129
+ this.strings.set(locale, unwrap(module));
130
+ }, (error) => {
131
+ this.loading.delete(locale);
132
+ throw error;
133
+ });
134
+ this.loading.set(locale, loading);
135
+ }
136
+ return loading;
137
+ }
138
+ };
139
+ /**
85
140
  * The text the UI shows, in the chosen language. Starts in the fallback locale; `useDeviceLocale` switches to the best
86
141
  * match for the device, and `setLocale` to any available locale. Locale files load when first used, without reloading
87
142
  * the page. Strings missing from a locale come from the fallback locale, then the key itself.
88
143
  *
144
+ * Other projects can add their own strings and locales with `addLocales` and `addStrings`. A locale's strings from
145
+ * every set load together and are merged in the order the sets were added (PaintCanvas's first), so a key in a later
146
+ * set replaces the same key in an earlier one. Keys are prefixed by project by convention: `paintCanvas.` for
147
+ * PaintCanvas's, and e.g. `liveCanvas.` for an app's.
148
+ *
89
149
  * Dispatches a `change` event when the locale or its strings change, so the UI can show the new text.
90
150
  *
91
151
  * @example
92
152
  * const translations = new Translations({ locales: { "en-US": ..., ja: () => import("./ja.json") } });
93
153
  * await translations.useDeviceLocale();
94
- * translations.t("layers.delete", { name: "Sketch" });
154
+ * translations.t("paintCanvas.layers.delete", { name: "Sketch" });
95
155
  */
96
156
  var Translations = class extends EventTarget {
97
- loaders;
98
157
  fallback;
99
- /** Strings of each locale loaded so far, plus any added */
100
- loaded = /* @__PURE__ */ new Map();
158
+ /** Sets of strings, in the order they were added */
159
+ sources;
160
+ /** Each locale's strings from every set loaded so far, merged in order */
161
+ merged = /* @__PURE__ */ new Map();
101
162
  current;
102
163
  /** The locale most recently asked for, so a slow load can't override a later choice */
103
164
  requested;
104
165
  constructor(options = {}) {
105
166
  super();
106
- this.loaders = options.locales ?? BUILT_IN_LOCALES;
107
167
  this.fallback = options.fallback ?? "en-US";
108
- this.loaded.set(this.fallback, options.fallbackStrings ?? en_US_default);
168
+ const builtIn = new LocaleSource(options.locales ?? BUILT_IN_LOCALES);
169
+ builtIn.strings.set(this.fallback, options.fallbackStrings ?? en_US_default);
170
+ this.sources = [builtIn];
171
+ this.merge(this.fallback);
109
172
  this.current = this.fallback;
110
173
  }
111
174
  /** The locale in use (an IETF BCP 47 tag, e.g. `ja`) */
112
175
  get locale() {
113
176
  return this.current;
114
177
  }
115
- /** Locales that can be used */
178
+ /** Locales that can be used: the fallback, and every locale a set of strings can load */
116
179
  get locales() {
117
- return [.../* @__PURE__ */ new Set([this.fallback, ...Object.keys(this.loaders)])];
180
+ return [.../* @__PURE__ */ new Set([this.fallback, ...this.sources.flatMap((source) => Object.keys(source.loaders))])];
118
181
  }
119
182
  /**
120
183
  * Switch to the best available locale for the device's languages (`navigator.languages`).
@@ -123,8 +186,8 @@ var Translations = class extends EventTarget {
123
186
  return this.setLocale(matchLocale(languages, this.locales, this.fallback));
124
187
  }
125
188
  /**
126
- * Switch to a locale (the best available match for the tag), loading its strings if needed. Dispatches `change` once
127
- * its strings are ready. If loading fails, the current locale stays and this rejects.
189
+ * Switch to a locale (the best available match for the tag), loading its strings from every set if needed.
190
+ * Dispatches `change` once they're ready. If loading fails, the current locale stays and this rejects.
128
191
  *
129
192
  * @example
130
193
  * await translations.setLocale("zh-TW"); // uses zh-Hant
@@ -132,35 +195,67 @@ var Translations = class extends EventTarget {
132
195
  async setLocale(tag) {
133
196
  const locale = matchLocale([tag], this.locales, this.fallback);
134
197
  this.requested = locale;
135
- if (!this.loaded.has(locale)) {
136
- const module = await this.loaders[locale]();
137
- this.loaded.set(locale, "default" in module && typeof module.default === "object" ? module.default : module);
138
- }
198
+ await this.load(locale);
139
199
  if (this.requested !== locale) return;
140
200
  this.current = locale;
141
201
  this.dispatchEvent(new Event("change"));
142
202
  }
143
203
  /**
144
- * Add or replace strings for a locale (e.g. the implementer's own wording or extra keys). Dispatches `change` if it's
145
- * the locale in use or the fallback.
204
+ * Add a set of strings for some locales, loaded when first used, after the sets already added (so its keys replace
205
+ * theirs). Its locales become available. The locale in use and the fallback load straight away, then `change` is
206
+ * dispatched if the set has strings for either.
146
207
  *
147
208
  * @example
148
- * translations.addStrings("en-US", { "toolbar.brush": "Paintbrush" });
209
+ * await translations.addLocales({
210
+ * "en-US": () => import("./locales/en-US.json"),
211
+ * fr: () => import("./locales/fr.json"),
212
+ * });
213
+ * translations.t("liveCanvas.share");
214
+ */
215
+ async addLocales(loaders) {
216
+ this.sources.push(new LocaleSource(loaders));
217
+ const locales = [.../* @__PURE__ */ new Set([this.current, this.fallback])];
218
+ await Promise.all(locales.map((locale) => this.load(locale)));
219
+ if (locales.some((locale) => Object.hasOwn(loaders, locale))) this.dispatchEvent(new Event("change"));
220
+ }
221
+ /**
222
+ * Add or replace strings for a locale (e.g. the implementer's own wording or extra keys), after the sets already
223
+ * added. Dispatches `change` if it's the locale in use or the fallback.
224
+ *
225
+ * @example
226
+ * translations.addStrings("en-US", { "paintCanvas.tool.brush": "Paintbrush" });
149
227
  */
150
228
  addStrings(locale, strings) {
151
- this.loaded.set(locale, {
152
- ...this.loaded.get(locale),
153
- ...strings
154
- });
229
+ const source = new LocaleSource();
230
+ source.strings.set(locale, strings);
231
+ this.sources.push(source);
232
+ this.merge(locale);
155
233
  if (locale === this.current || locale === this.fallback) this.dispatchEvent(new Event("change"));
156
234
  }
157
235
  /**
236
+ * Load a locale's strings from every set, including sets added while loading, then merge them.
237
+ */
238
+ async load(locale) {
239
+ let count;
240
+ do {
241
+ count = this.sources.length;
242
+ await Promise.all(this.sources.map((source) => source.load(locale)));
243
+ } while (count !== this.sources.length);
244
+ this.merge(locale);
245
+ }
246
+ /**
247
+ * Merge a locale's strings loaded so far from every set, in the order the sets were added.
248
+ */
249
+ merge(locale) {
250
+ this.merged.set(locale, Object.assign({}, ...this.sources.map((source) => source.strings.get(locale))));
251
+ }
252
+ /**
158
253
  * Translate a key, putting values into its `{name}` placeholders. With a numeric `count`, the key's plural form for
159
- * the locale is used when there is one (e.g. `presets.imported.one` and `presets.imported.other`). Falls back to the
160
- * fallback locale, then the key.
254
+ * the locale is used when there is one (e.g. `paintCanvas.presets.imported.one` and
255
+ * `paintCanvas.presets.imported.other`). Falls back to the fallback locale, then the key.
161
256
  *
162
257
  * @example
163
- * translations.t("presets.imported", { count: 3 }); // "Imported 3 brush presets"
258
+ * translations.t("paintCanvas.presets.imported", { count: 3 }); // "Imported 3 brush presets"
164
259
  */
165
260
  t(key, values) {
166
261
  const count = values?.count;
@@ -170,7 +265,7 @@ var Translations = class extends EventTarget {
170
265
  key
171
266
  ] : [key];
172
267
  for (const locale of [this.current, this.fallback]) {
173
- const strings = this.loaded.get(locale);
268
+ const strings = this.merged.get(locale);
174
269
  const found = keys.find((each) => strings && Object.hasOwn(strings, each));
175
270
  if (found) return interpolate(strings[found], values);
176
271
  }
@@ -179,4 +274,4 @@ var Translations = class extends EventTarget {
179
274
  };
180
275
  /** The translations PaintCanvas's UI uses, unless an element is given others */
181
276
  var translations = new Translations();
182
- export { BUILT_IN_LOCALES, FALLBACK_LOCALE, Translations as default, interpolate, matchLocale, translations };
277
+ export { BUILT_IN_LOCALES, FALLBACK_LOCALE, Translations as default, interpolate, languageName, matchLocale, translations };