@gjsify/node-gi 0.49.0 → 0.51.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/binding.gyp CHANGED
@@ -35,7 +35,12 @@
35
35
  "-std=c++17"
36
36
  ],
37
37
  "libraries": [
38
- "<!@(pkg-config --libs girepository-2.0 cairo)"
38
+ "<!@(pkg-config --libs girepository-2.0 cairo)",
39
+ # libintl for the gettext binders (private.cc). Empty off darwin: glibc
40
+ # absorbs bindtextdomain/textdomain into libc, macOS's libSystem has no
41
+ # gettext at all and a two-level-namespace Mach-O must name the library
42
+ # it takes them from. See scripts/mac-intl-gyp-flags.mjs.
43
+ "<!@(node scripts/mac-intl-gyp-flags.mjs)"
39
44
  ],
40
45
  "xcode_settings": {
41
46
  "OTHER_CFLAGS": [
package/gettext.d.ts CHANGED
@@ -1,19 +1,19 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // @gjsify/node-gi/gettext — types for the GJS `Gettext` module on Node.
3
3
 
4
- /** Look up `msgid` in the default domain (passthrough). */
4
+ /** Look up `msgid` in the default domain. */
5
5
  export function gettext(msgid: string): string;
6
- /** Look up `msgid` in `domain` (passthrough). */
6
+ /** Look up `msgid` in `domain`. */
7
7
  export function dgettext(domain: string | null, msgid: string): string;
8
- /** Look up `msgid` in `domain`/`category` (passthrough). */
8
+ /** Look up `msgid` in `domain`/`category`. */
9
9
  export function dcgettext(domain: string | null, msgid: string, category: number): string;
10
- /** Plural lookup in the default domain (passthrough). */
10
+ /** Plural lookup in the default domain. */
11
11
  export function ngettext(msgid1: string, msgid2: string, n: number): string;
12
- /** Plural lookup in `domain` (passthrough). */
12
+ /** Plural lookup in `domain`. */
13
13
  export function dngettext(domain: string | null, msgid1: string, msgid2: string, n: number): string;
14
- /** Context lookup in the default domain (passthrough). */
14
+ /** Context lookup in the default domain. */
15
15
  export function pgettext(context: string, msgid: string): string;
16
- /** Context lookup in `domain` (passthrough). */
16
+ /** Context lookup in `domain`. */
17
17
  export function dpgettext(domain: string | null, context: string, msgid: string): string;
18
18
  /** Domain-bound gettext bindings. */
19
19
  export function domain(domainName: string): {
@@ -21,15 +21,15 @@ export function domain(domainName: string): {
21
21
  ngettext(msgid1: string, msgid2: string, n: number): string;
22
22
  pgettext(context: string, msgid: string): string;
23
23
  };
24
- /** Set the locale for `category` (no-op on Node). */
25
- export function setlocale(category: number, locale: string | null): null;
26
- /** Set the default text domain (no-op on Node). */
27
- export function textdomain(domainName: string | null): null;
28
- /** Bind a text domain to a directory (no-op on Node). */
29
- export function bindtextdomain(domainName: string, dirName: string | null): null;
30
- /** Set the output codeset for a text domain (no-op on Node). */
24
+ /** Set the locale for `category`, or report it when `locale` is null. */
25
+ export function setlocale(category: number, locale: string | null): string | null;
26
+ /** Set the default text domain, or report it when `domainName` is null. */
27
+ export function textdomain(domainName: string | null): string | null;
28
+ /** Bind a text domain to the directory its catalogs live in; pins UTF-8. */
29
+ export function bindtextdomain(domainName: string, dirName: string | null): string | null;
30
+ /** Set the output codeset for a text domain. No-op — `bindtextdomain` pins UTF-8. */
31
31
  export function bindtextdomainCodeset(domainName: string, codeset: string | null): null;
32
- /** The standard POSIX locale category constants. */
32
+ /** The POSIX locale categories, read from the host C library's own <locale.h>. */
33
33
  export const LocaleCategory: {
34
34
  CTYPE: number;
35
35
  NUMERIC: number;
@@ -54,9 +54,9 @@ export interface GettextModule {
54
54
  ngettext(msgid1: string, msgid2: string, n: number): string;
55
55
  pgettext(context: string, msgid: string): string;
56
56
  };
57
- setlocale(category: number, locale: string | null): null;
58
- textdomain(domainName: string | null): null;
59
- bindtextdomain(domainName: string, dirName: string | null): null;
57
+ setlocale(category: number, locale: string | null): string | null;
58
+ textdomain(domainName: string | null): string | null;
59
+ bindtextdomain(domainName: string, dirName: string | null): string | null;
60
60
  bindtextdomainCodeset(domainName: string, codeset: string | null): null;
61
61
  LocaleCategory: typeof LocaleCategory;
62
62
  }
package/gettext.js CHANGED
@@ -2,108 +2,195 @@
2
2
  // @gjsify/node-gi/gettext — the GJS `Gettext` module on Node.
3
3
  //
4
4
  // GJS exposes a built-in `gettext` module (`import Gettext from 'gettext'` /
5
- // `imports.gettext`) — a convenience layer over GLib's gettext family. On Node
6
- // we provide a no-translation passthrough: every lookup returns the untranslated
7
- // msgid, which is exactly the correct fallback when no message catalog is bound
8
- // (GLib's gettext degrades to the same passthrough with no catalog). This keeps
9
- // GJS source that calls `Gettext.gettext(...)` at module load working unchanged
10
- // on Node without pulling in a real gettext binding.
5
+ // `imports.gettext`) — a convenience layer over GLib's gettext family. This is the
6
+ // same layer with the same split of responsibilities as GJS's:
7
+ // • LOOKUPS go to GLib (`g_dgettext` / `g_dngettext` / `g_dpgettext2`), which is
8
+ // introspectable and therefore needs no native code here;
9
+ // • the BINDERS (`setlocale` / `textdomain` / `bindtextdomain`) are libintl, which
10
+ // has no GIR at all, so they come from the addon — the same reason GJS routes
11
+ // them through GjsPrivate instead of `imports.gi.GLib`.
12
+ // Reference: refs/gjs/modules/core/_gettext.js, function for function.
13
+ //
14
+ // THIS MODULE USED TO BE A PASSTHROUGH — every lookup returned its msgid, every
15
+ // binder a no-op returning null. That is the right fallback when no catalog can be
16
+ // bound, and it was written when node-gi had no way to bind one; what made it a
17
+ // defect is that it stayed after the rest of the bridge could. It was also only
18
+ // HALF of what was wrong — the process locale (private.cc) had to be fixed too, or
19
+ // a real `bindtextdomain` would still have translated nothing.
11
20
  //
12
21
  // This is the single source of truth for the Gettext surface: the legacy
13
22
  // `imports.gettext` exposed by `@gjsify/node-gi/globals` re-uses this module's
14
23
  // default export. The gjsify `--app node` build aliases the bare `gettext`
15
24
  // specifier to this module (kept external — `ALIASES_GJS_FOR_NODE`).
25
+ import * as native from './index.js';
26
+ import { requireGi } from './gi.js';
27
+
28
+ // GLib is resolved on first lookup, not at import: loading a namespace walks the
29
+ // typelib, and a module imported for its LocaleCategory alone should not pay for
30
+ // it. The addon itself is already loaded — `./index.js` above is what puts the
31
+ // process in the environment's locale, and that has to happen whether or not
32
+ // anything ever looks a string up.
33
+ let glib = null;
34
+ function GLib() {
35
+ if (glib === null) glib = requireGi('GLib', '2.0');
36
+ return glib;
37
+ }
38
+
39
+ // THE ADDON AND THIS JS CAN BE PAIRED ACROSS VERSIONS, so the binders are probed
40
+ // rather than assumed — the same reason `native-prebuilds.js` probes
41
+ // `prependLibraryPath`. `gtk-os-suites.yml`'s shipped-closure legs stage the
42
+ // PUBLISHED addon against THIS tree's JS deliberately ("the tarball a consumer
43
+ // actually receives"), and a bare `native.localeCategories()` died there with
44
+ // `TypeError: native.localeCategories is not a function` — taking the whole module
45
+ // down, and `globals.js` with it, because it is evaluated at import.
16
46
  //
17
- // Reference: GJS's `gettext` module (refs/gjs/modules/esm/gettext.js +
18
- // modules/core/_gettext.js). Signatures mirror that surface.
47
+ // Degrading means behaving exactly as an addon without the binders always did:
48
+ // lookups still work (they go through GLib, which every addon version has), and
49
+ // the binders are the no-ops they used to be. It is announced once, because a
50
+ // silently untranslated UI is the defect this file exists to close.
51
+ const haveLocaleBinders = typeof native.localeCategories === 'function';
52
+
53
+ let announcedStaleAddon = false;
54
+ function noLocaleBinders(fn) {
55
+ if (!announcedStaleAddon) {
56
+ announcedStaleAddon = true;
57
+ process.emitWarning(
58
+ `@gjsify/node-gi: the loaded native addon predates the gettext binders, so ` +
59
+ `${fn}() cannot take effect and messages stay untranslated. Rebuild the addon ` +
60
+ `(npm run rebuild) or update @gjsify/node-gi.`,
61
+ 'NodeGiStaleAddon',
62
+ );
63
+ }
64
+ return null;
65
+ }
19
66
 
20
- /** Look up `msgid` in the default domain. Passthrough — returns `msgid`. */
67
+ // The POSIX categories per C library, used ONLY when the addon cannot report its
68
+ // own. The addon is the source of truth precisely because these are C-library
69
+ // constants rather than a standard; this table exists so a stale pairing gets the
70
+ // right numbers for its platform instead of glibc's everywhere, which is the bug
71
+ // the module used to ship.
72
+ const FALLBACK_CATEGORIES = {
73
+ // glibc
74
+ linux: { CTYPE: 0, NUMERIC: 1, TIME: 2, COLLATE: 3, MONETARY: 4, MESSAGES: 5, ALL: 6 },
75
+ // Darwin libc
76
+ darwin: { ALL: 0, COLLATE: 1, CTYPE: 2, MONETARY: 3, NUMERIC: 4, TIME: 5, MESSAGES: 6 },
77
+ // The MSVC CRT, which has no LC_MESSAGES — GNU gettext's <libintl.h> picks 1729.
78
+ win32: { ALL: 0, COLLATE: 1, CTYPE: 2, MONETARY: 3, NUMERIC: 4, TIME: 5, MESSAGES: 1729 },
79
+ };
80
+
81
+ /** Look up `msgid` in the default domain (the one `textdomain()` last named). */
21
82
  export function gettext(msgid) {
22
- return msgid;
83
+ return GLib().dgettext(null, msgid);
23
84
  }
24
85
 
25
- /** Look up `msgid` in `domain`. Passthrough — returns `msgid`. */
26
- export function dgettext(_domain, msgid) {
27
- return msgid;
86
+ /** Look up `msgid` in `domain`. */
87
+ export function dgettext(domain, msgid) {
88
+ return GLib().dgettext(domain, msgid);
28
89
  }
29
90
 
30
- /** Look up `msgid` in `domain`/`category`. Passthrough — returns `msgid`. */
31
- export function dcgettext(_domain, msgid, _category) {
32
- return msgid;
91
+ /** Look up `msgid` in `domain` for `category`. */
92
+ export function dcgettext(domain, msgid, category) {
93
+ return GLib().dcgettext(domain, msgid, category);
33
94
  }
34
95
 
35
- /** Plural lookup in the default domain. Passthrough — singular for n===1, else plural. */
96
+ /** Plural lookup in the default domain. */
36
97
  export function ngettext(msgid1, msgid2, n) {
37
- return n === 1 ? msgid1 : msgid2;
98
+ return GLib().dngettext(null, msgid1, msgid2, n);
38
99
  }
39
100
 
40
- /** Plural lookup in `domain`. Passthrough — singular for n===1, else plural. */
41
- export function dngettext(_domain, msgid1, msgid2, n) {
42
- return n === 1 ? msgid1 : msgid2;
101
+ /** Plural lookup in `domain`. */
102
+ export function dngettext(domain, msgid1, msgid2, n) {
103
+ return GLib().dngettext(domain, msgid1, msgid2, n);
43
104
  }
44
105
 
45
- /** Context lookup in the default domain. Passthrough — returns `msgid`. */
46
- export function pgettext(_context, msgid) {
47
- return msgid;
106
+ /** Context lookup in the default domain. */
107
+ export function pgettext(context, msgid) {
108
+ return GLib().dpgettext2(null, context, msgid);
48
109
  }
49
110
 
50
- /** Context lookup in `domain`. Passthrough — returns `msgid`. */
51
- export function dpgettext(_domain, _context, msgid) {
52
- return msgid;
111
+ /** Context lookup in `domain`. */
112
+ export function dpgettext(domain, context, msgid) {
113
+ return GLib().dpgettext2(domain, context, msgid);
53
114
  }
54
115
 
55
116
  /**
56
117
  * Create gettext bindings bound to a particular translation domain. Mirrors
57
118
  * GJS's `Gettext.domain()` — returns an object with `gettext`/`ngettext`/
58
- * `pgettext` bound to `domainName` (passthrough here).
119
+ * `pgettext` bound to `domainName`.
59
120
  */
60
- export function domain(_domainName) {
121
+ export function domain(domainName) {
61
122
  return {
62
123
  gettext(msgid) {
63
- return msgid;
124
+ return GLib().dgettext(domainName, msgid);
64
125
  },
65
126
  ngettext(msgid1, msgid2, n) {
66
- return n === 1 ? msgid1 : msgid2;
127
+ return GLib().dngettext(domainName, msgid1, msgid2, n);
67
128
  },
68
- pgettext(_context, msgid) {
69
- return msgid;
129
+ pgettext(context, msgid) {
130
+ return GLib().dpgettext2(domainName, context, msgid);
70
131
  },
71
132
  };
72
133
  }
73
134
 
74
- /** Set the locale for `category`. No-op on Node — returns null. */
75
- export function setlocale(_category, _locale) {
76
- return null;
135
+ /**
136
+ * Set the locale for `category`, or report it when `locale` is null.
137
+ *
138
+ * Thread-local, as GJS's is (`GjsPrivate.set_thread_locale` → `uselocale`), and it
139
+ * inherits GJS's reporting quirk with it: a query answers with the name that was
140
+ * in effect BEFORE the last successful set on this thread, because the report
141
+ * comes from the global locale `uselocale` does not touch. Measured against
142
+ * gjs 1.88 — setting LC_MESSAGES to "C" stops translation immediately while the
143
+ * next query still answers the old name.
144
+ *
145
+ * @returns {string|null} the prior locale name, or null if it could not be set
146
+ */
147
+ export function setlocale(category, locale) {
148
+ if (!haveLocaleBinders) return noLocaleBinders('setlocale');
149
+ return native.setThreadLocale(category, locale ?? null);
77
150
  }
78
151
 
79
- /** Set the default text domain. No-op on Node — returns null. */
80
- export function textdomain(_domainName) {
81
- return null;
152
+ /** Set the default text domain, or report it when `domainName` is null. */
153
+ export function textdomain(domainName) {
154
+ if (!haveLocaleBinders) return noLocaleBinders('textdomain');
155
+ return native.textdomain(domainName ?? null);
82
156
  }
83
157
 
84
- /** Bind a text domain to a directory. No-op on Node — returns null. */
85
- export function bindtextdomain(_domainName, _dirName) {
86
- return null;
158
+ /**
159
+ * Bind a text domain to the directory its catalogs live in.
160
+ *
161
+ * The codeset is pinned to UTF-8 alongside the bind, exactly as
162
+ * `gjs_bindtextdomain` does — see the note in private.cc.
163
+ */
164
+ export function bindtextdomain(domainName, dirName) {
165
+ if (!haveLocaleBinders) return noLocaleBinders('bindtextdomain');
166
+ return native.bindtextdomain(domainName, dirName ?? null);
87
167
  }
88
168
 
89
- /** Set the output codeset for a text domain. No-op on Node — returns null. */
169
+ /**
170
+ * Set the output codeset for a text domain.
171
+ *
172
+ * `bindtextdomain()` already pins UTF-8, which is what every string crossing this
173
+ * bridge must be, so this stays a no-op returning null. It is the one member of
174
+ * the surface GJS does not implement either — it is absent from
175
+ * refs/gjs/modules/core/_gettext.js — and is kept for source that calls it.
176
+ */
90
177
  export function bindtextdomainCodeset(_domainName, _codeset) {
91
178
  return null;
92
179
  }
93
180
 
94
181
  /**
95
- * The `LocaleCategory` enum — the standard POSIX locale category constants GJS
96
- * surfaces via `GjsPrivate.LocaleCategory`.
182
+ * The `LocaleCategory` enum — the POSIX locale categories GJS surfaces via
183
+ * `GjsPrivate.LocaleCategory`.
184
+ *
185
+ * Read from THIS platform's `<locale.h>` rather than written down: the numbers are
186
+ * C-library constants, not a standard (LC_MESSAGES is 5 on glibc and 6 on darwin),
187
+ * so the literal table this module used to carry addressed LC_TIME on macOS while
188
+ * claiming to address LC_MESSAGES — on two of the three platforms the translation
189
+ * blocker was reported from.
97
190
  */
98
- export const LocaleCategory = {
99
- CTYPE: 0,
100
- NUMERIC: 1,
101
- TIME: 2,
102
- COLLATE: 3,
103
- MONETARY: 4,
104
- MESSAGES: 5,
105
- ALL: 6,
106
- };
191
+ export const LocaleCategory = haveLocaleBinders
192
+ ? native.localeCategories()
193
+ : (FALLBACK_CATEGORIES[process.platform] ?? FALLBACK_CATEGORIES.linux);
107
194
 
108
195
  /**
109
196
  * The GJS `Gettext` module as a default export — the object shape
package/gi.js CHANGED
@@ -832,31 +832,99 @@ function assignTemplateChildren(instance, handle, reg) {
832
832
  }
833
833
  }
834
834
 
835
- // Push construct-time GObject-property values through the class's own JS SETTERS.
835
+ // The prototype SETTER a GObject property name resolves to, across the three
836
+ // spellings GJS makes equivalent, or undefined when the class declares none.
836
837
  //
837
- // A class can declare a GObject property (`Properties: { displayWidth: ParamSpec }`,
838
- // often CONSTRUCT with a default) AND a matching JS accessor
839
- // (`get/set displayWidth` over a `_displayWidth` backing field). On GJS the property
840
- // set vfunc delegates to the JS setter (`JS_SetProperty`), so the CONSTRUCT default
841
- // reaches `_displayWidth` at construction. node-gi builds the wrapper AFTER
842
- // g_object_new, so at construct time there is no USER_PROTO to route through — the
843
- // value lands only in the engine's per-instance store, and the class's getter
838
+ // GJS's set_property vfunc writes only the UNDERSCORE spelling
839
+ // (refs/gjs/gi/gobject.cpp jsobj_set_gproperty → JS_SetProperty(underscore_name)),
840
+ // and reaches a camelCase accessor anyway because registerClass has already mirrored
841
+ // whichever spelling the class declared onto the dash, underscore AND camel names
842
+ // (`_checkAccessors`, refs/gjs/modules/core/_common.js). node-gi keeps the engine
843
+ // store as the backing store rather than installing GJS's generated accessors, so it
844
+ // resolves the same three spellings at the point of use instead — same reachability,
845
+ // no accessor synthesis. Learn6502's SourceView is exactly why this matters: its
846
+ // property is `line-numbers` and its accessor is `lineNumbers`.
847
+ function findPropertySetter(userProto, propertyName) {
848
+ const underscored = propertyName.replace(/-/g, '_');
849
+ const camel = propertyName.replace(/-([a-z])/g, (m) => m[1].toUpperCase());
850
+ for (const spelling of [propertyName, underscored, camel]) {
851
+ const desc = findProtoDescriptor(userProto, spelling);
852
+ if (desc !== undefined && typeof desc.set === 'function') return desc.set;
853
+ }
854
+ return undefined;
855
+ }
856
+
857
+ // Run a class's own JS setter for a custom property the ENGINE has just stored —
858
+ // the L1 half of NodeGiSetProperty (src/class.cc), which is node-gi's
859
+ // gjs_object_set_gproperty. The engine calls this only once the instance HAS a
860
+ // wrapper, so a set that lands during construction never reaches here; those are
861
+ // replayed by flushPropertiesToJsSetters below.
862
+ function runJsPropertySetter(handle, propertyName) {
863
+ const instance = instanceCache.get(handle);
864
+ if (instance === undefined) return;
865
+ const up = instance[USER_PROTO];
866
+ if (up === undefined) return;
867
+ const set = findPropertySetter(up, propertyName);
868
+ if (set === undefined) return; // no JS accessor: the store is the single backing store
869
+ set.call(instance, wrapReturn(native.getProperty(handle, propertyName)));
870
+ }
871
+ // Guarded because the addon paired with this JS may PREDATE it, and that pairing is
872
+ // deliberate rather than hypothetical: gtk-os-suites.yml's shipped-closure legs run
873
+ // this checkout's JS against the PUBLISHED addon (`NODE_GI_NATIVE=prebuild`, logged
874
+ // as "node-gi JS: (this checkout) / addon: (published)"), so a bare call to a new
875
+ // native export takes those three legs down with `native.<new export> is not a
876
+ // function` until the next release republishes the binary. Every native call added
877
+ // here degrades instead; without the callback, a post-construction set simply does
878
+ // not reach the JS setter, which is what 0.50.0 did.
879
+ if (typeof native.setPropertySetCallback === 'function') {
880
+ native.setPropertySetCallback(runJsPropertySetter);
881
+ }
882
+
883
+ // Which properties the construct-time replay must run.
884
+ //
885
+ // `native.storedPropertyNames` is the honest answer — what was ACTUALLY set, in
886
+ // first-set order. An addon older than this JS does not export it (same cross-version
887
+ // pairing as the guard above), so the fallback is the CONSTRUCT-flag filter this used
888
+ // to be: narrower, since it misses a plain property passed to `new`, but never wrong
889
+ // — GObject does apply CONSTRUCT properties at construction. Keeping it is what makes
890
+ // the shipped-closure legs measure the published addon rather than this gap.
891
+ function propertiesToReplay(handle, reg) {
892
+ if (typeof native.storedPropertyNames === 'function') return native.storedPropertyNames(handle);
893
+ return reg.constructPropertyNames ?? [];
894
+ }
895
+
896
+ // Replay the properties set DURING construction through the class's own JS SETTERS.
897
+ //
898
+ // A class can declare a GObject property (`Properties: { displayWidth: ParamSpec }`)
899
+ // AND a matching JS accessor (`get/set displayWidth` over a `_displayWidth` backing
900
+ // field). On GJS the set vfunc delegates to the JS setter, so a value applied by
901
+ // g_object_new reaches `_displayWidth` at construction. node-gi builds the wrapper
902
+ // AFTER g_object_new, so during construction there is no USER_PROTO to route through
903
+ // — the value lands only in the engine's per-instance store, and the class's getter
844
904
  // (reading `_displayWidth`) sees `undefined` (the Learn6502 Display "DrawingArea is
845
905
  // required" wall). Once USER_PROTO is attached (here, right after construction), read
846
- // each such property's construct value back out (from the store, or the ParamSpec
847
- // default) and run it through the JS setter — the node-gi analogue of GJS applying a
848
- // CONSTRUCT property via the JS setter, and BEFORE the user ctor body runs (same
849
- // order as GJS). Only properties whose name resolves to a prototype SETTER are
850
- // touched, so a plain GObject property (no JS accessor) keeps the store as its single
851
- // backing store — unchanged.
852
- function flushConstructProperties(instance, handle, reg) {
853
- if (reg.constructPropertyNames === undefined) return;
906
+ // each such value back out and run it through the JS setter, BEFORE the user ctor
907
+ // body runs — the same order GJS produces.
908
+ //
909
+ // WHICH properties: the ones the engine actually STORED, i.e. that something really
910
+ // set (`native.storedPropertyNames`). Not every declared property — flushing those
911
+ // runs a setter for a property nobody assigned, against state the ctor body has not
912
+ // created yet, which is the Learn6502 SourceView `selectable` → `_signalHandlers`
913
+ // forEach crash. "Was it set?" is the honest question and the store is its honest
914
+ // answer; the CONSTRUCT flag is only a proxy for it (GObject applies a CONSTRUCT
915
+ // property's default at construction, so it is stored too, and a plain READWRITE
916
+ // property passed to `new` is stored as well — which GJS also routes through the
917
+ // setter, and the old CONSTRUCT-only filter missed).
918
+ //
919
+ // Only properties whose name resolves to a prototype SETTER are touched, so a plain
920
+ // GObject property (no JS accessor) keeps the store as its single backing store.
921
+ function flushPropertiesToJsSetters(instance, handle, reg) {
854
922
  const up = instance[USER_PROTO];
855
923
  if (up === undefined) return;
856
- for (const name of reg.constructPropertyNames) {
857
- const desc = findProtoDescriptor(up, name);
858
- if (desc === undefined || typeof desc.set !== 'function') continue;
859
- desc.set.call(instance, wrapReturn(native.getProperty(handle, name)));
924
+ for (const name of propertiesToReplay(handle, reg)) {
925
+ const set = findPropertySetter(up, name);
926
+ if (set === undefined) continue;
927
+ set.call(instance, wrapReturn(native.getProperty(handle, name)));
860
928
  }
861
929
  }
862
930
 
@@ -1605,7 +1673,7 @@ function makeClass(namespace, typeName) {
1605
1673
  assignTemplateChildren(instance, handle, reg);
1606
1674
  // Route construct-time property values through the class's JS setters now
1607
1675
  // that USER_PROTO is attached — before the user ctor body runs (GJS order).
1608
- flushConstructProperties(instance, handle, reg);
1676
+ flushPropertiesToJsSetters(instance, handle, reg);
1609
1677
  return instance;
1610
1678
  }
1611
1679
  // `nt` is a subclass of this introspected GObject class but was NEVER passed to
@@ -2340,13 +2408,11 @@ function registerClass(metaOrClass, maybeClass) {
2340
2408
  // Record the registration so the introspected base ctor (makeClass) routes
2341
2409
  // `new X(args)` (where new.target === klass) to constructType(typeHandle, …) +
2342
2410
  // the canonical wrapper. Template children move here from the old Subclass.
2343
- // The names of CONSTRUCT / CONSTRUCT_ONLY properties — the ONLY ones GObject
2344
- // applies at construction. The flush (flushConstructProperties) must be limited to
2345
- // these: a plain READWRITE property is NOT set during construction, so running its
2346
- // JS setter early (GObject never would) fires the setter's side effects against
2347
- // still-uninitialised instance state (the SourceView `selectable` → `_signalHandlers`
2348
- // forEach crash). Matches GJS, which only runs setters for props GObject applies at
2349
- // construct. Flags are the ABI-stable G_PARAM_CONSTRUCT (1<<2) | CONSTRUCT_ONLY (1<<3).
2411
+ // Which properties the construct-time flush replays is a question about this
2412
+ // INSTANCE ("what was actually set?"), and the engine's per-instance store answers
2413
+ // it directly. These CONSTRUCT / CONSTRUCT_ONLY names are only the FALLBACK for an
2414
+ // addon too old to answer it — see propertiesToReplay. Flags are the ABI-stable
2415
+ // G_PARAM_CONSTRUCT (1<<2) | CONSTRUCT_ONLY (1<<3).
2350
2416
  const PARAM_CONSTRUCT_MASK = 0x4 | 0x8;
2351
2417
  const constructPropertyNames = properties
2352
2418
  .filter((p) => typeof p.flags === 'number' && (p.flags & PARAM_CONSTRUCT_MASK) !== 0)
@@ -2356,7 +2422,6 @@ function registerClass(metaOrClass, maybeClass) {
2356
2422
  typeHandle,
2357
2423
  children: children.length > 0 ? children : undefined,
2358
2424
  internalChildren: internalChildren.length > 0 ? internalChildren : undefined,
2359
- // CONSTRUCT-property names, for the construct-property flush (below).
2360
2425
  constructPropertyNames: constructPropertyNames.length > 0 ? constructPropertyNames : undefined,
2361
2426
  });
2362
2427
  // Reverse index for runCtorForCObject: the engine identifies a C-created instance
package/globals.d.ts CHANGED
@@ -13,7 +13,7 @@ export interface GjsImports {
13
13
  };
14
14
  /** `imports.system` — the `@gjsify/node-gi/system` module (process identity + lifecycle). */
15
15
  system: SystemModule;
16
- /** `imports.gettext` — the `@gjsify/node-gi/gettext` module (no-translation passthrough). */
16
+ /** `imports.gettext` — the `@gjsify/node-gi/gettext` module (real gettext via GLib + libintl). */
17
17
  gettext: GettextModule;
18
18
  /** `imports.byteArray` — the legacy GJS byte-array module (GJS semantics: zero-terminated, fatal decode). */
19
19
  byteArray: {
package/gtk-runtime.js CHANGED
@@ -437,6 +437,28 @@ export function maybeWireGtkWindowingEnv() {
437
437
  setIfUnset('FONTCONFIG_FILE', fontsConf);
438
438
  }
439
439
 
440
+ // THE FACES, which are a different question from the two lines above and on one of the
441
+ // two platforms not answerable by an environment variable at all.
442
+ //
443
+ // `XDG_DATA_DIRS` already reaches `<bundle>/share/fonts` wherever fontconfig drives
444
+ // Pango — its stock configuration carries `<dir prefix="xdg">fonts</dir>` — so on darwin
445
+ // the set above is enough and this variable is a second, cheaper route to the same files.
446
+ // On WIN32 it is the only route there is: GTK4 uses pangowin32, whose font map is filled
447
+ // exclusively by `pango_font_map_dwrite_populate()` from the DirectWrite system
448
+ // collection, and a `FONTCONFIG_FILE` naming a directory of faces moves that map by ZERO
449
+ // families even when it is the only configuration present (measured on Windows 11 /
450
+ // GTK 4.22.4, both directions — ADR 0038 § W1-W5). The face has to be handed to the map
451
+ // through `add_font_file`, which is a RUNTIME call somebody has to make.
452
+ //
453
+ // So the loader's job here is to name the directory, not to register anything: it runs
454
+ // before the addon loads and has no Pango to talk to. `@gjsify/gtk-host`'s `initFonts()`
455
+ // is the reader — the same handover `gjsify ship` makes for an application's OWN faces
456
+ // through `GJSIFY_FONT_DIR`, one layer down, because these faces belong to the RUNTIME
457
+ // and every application on it needs them. Two variables and not one: an app that ships a
458
+ // brand face must not have to choose between its face and the platform's.
459
+ const runtimeFontDir = join(shareDir, 'fonts');
460
+ if (existsSync(runtimeFontDir)) setIfUnset('GJSIFY_GTK_RUNTIME_FONT_DIR', runtimeFontDir);
461
+
440
462
  // GStreamer, which finds its plugins the way GTK finds its schemas: by env,
441
463
  // read at init. Without this the bundle can ship every Gst typelib and still
442
464
  // play nothing — `Gst.init()` succeeds against an EMPTY registry, so the
package/index.js CHANGED
@@ -277,9 +277,29 @@ export const setTemplateCallbackResolver = native.setTemplateCallbackResolver;
277
277
  // Reflect.constructs the class in adopt mode — see gi.js runCtorForCObject.
278
278
  export const setConstructCallback = native.setConstructCallback;
279
279
 
280
+ // Registers the L1 callback the engine's set_property vfunc invokes after storing a
281
+ // custom property: given (instanceHandle, propertyName) it runs the class's own JS
282
+ // setter — see gi.js runJsPropertySetter. The engine calls it only once the instance
283
+ // HAS a wrapper, so a construct-time set never reaches it; those are replayed from
284
+ // the base ctor over storedPropertyNames.
285
+ export const setPropertySetCallback = native.setPropertySetCallback;
286
+
287
+ // The custom properties actually SET on an instance (the per-instance store's keys) —
288
+ // what that construct-time replay has to push through the class's JS setters.
289
+ export const storedPropertyNames = native.storedPropertyNames;
290
+
280
291
  export const logSetWriterFunc = native.logSetWriterFunc;
281
292
  export const logSetWriterDefault = native.logSetWriterDefault;
282
293
  export const bindPropertyFull = native.bindPropertyFull;
283
294
  export const bindingGroupBindFull = native.bindingGroupBindFull;
284
295
 
296
+ // The libintl half of GjsPrivate — the binders GLib's GIR does not publish, plus
297
+ // the LC_* constants read from the host's own <locale.h>. Consumed by
298
+ // `./gettext.js`; loading THIS module is also what puts the process in the
299
+ // environment's locale (the addon's Init does it, as gjs's entry point does).
300
+ export const setThreadLocale = native.setThreadLocale;
301
+ export const textdomain = native.textdomain;
302
+ export const bindtextdomain = native.bindtextdomain;
303
+ export const localeCategories = native.localeCategories;
304
+
285
305
  export default native;
@@ -71,13 +71,13 @@ export function createPackage({ requireGi, gettext, imports }) {
71
71
  },
72
72
 
73
73
  initGettext() {
74
+ // No try/catch: these were no-ops returning null when node-gi had no
75
+ // gettext binding, and the catch that tolerated that would now swallow
76
+ // the one failure worth seeing — a localedir that does not hold the
77
+ // app's catalogs, whose only other symptom is an untranslated UI.
74
78
  if (pkg.name) {
75
- try {
76
- gettext.bindtextdomain?.(pkg.name, pkg.localedir ?? null);
77
- gettext.textdomain?.(pkg.name);
78
- } catch {
79
- /* passthrough gettext — no catalog, ignore */
80
- }
79
+ gettext.bindtextdomain(pkg.name, pkg.localedir ?? null);
80
+ gettext.textdomain(pkg.name);
81
81
  }
82
82
  // The globals GJS apps + compiled Blueprint use pervasively.
83
83
  globalThis._ = (msgid) => gettext.gettext(msgid);