gt-vue 0.1.0-iris.1 → 0.1.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,38 @@
1
1
  # gt-vue
2
2
 
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#2012](https://github.com/generaltranslation/gt/pull/2012) [`8d376e2`](https://github.com/generaltranslation/gt/commit/8d376e23c80828dfd8756e2f0fbf0f7725e0f178) Thanks [@eoinest](https://github.com/eoinest)! - Add a lightweight Vue 3 runtime with catalog-backed string and rich-content
8
+ translation, cookie-backed reactive locale switching, child-only variables,
9
+ and typed value props for number, currency, and date formatting. Browser SPAs
10
+ restore the locale from a configurable cookie, while an explicit server locale
11
+ wins during SSR hydration.
12
+
13
+ ### Patch Changes
14
+
15
+ - [#2068](https://github.com/generaltranslation/gt/pull/2068) [`f2204b9`](https://github.com/generaltranslation/gt/commit/f2204b990278865c54d14d337392930ea1ec31bf) Thanks [@eoinest](https://github.com/eoinest)! - Translate statically authored custom-component default slots, preserve authored
16
+ Fragments, and align Branch and Plural wire values, rendering fallbacks, and
17
+ locale selection with React. Add React-compatible `context`, `id`, `maxChars`,
18
+ and `requiresReview` metadata to `<T>`, including compiler-facing `$` aliases.
19
+
20
+ - [#2080](https://github.com/generaltranslation/gt/pull/2080) [`5d8b78a`](https://github.com/generaltranslation/gt/commit/5d8b78a1085d5dadc5bdc7435b3c6addf8981c38) Thanks [@eoinest](https://github.com/eoinest)! - Migrate the package license from FSL-1.1-ALv2 to MIT.
21
+
22
+ - [#2090](https://github.com/generaltranslation/gt/pull/2090) [`95b48df`](https://github.com/generaltranslation/gt/commit/95b48df677d27f41a336f13e01ae7aa9aa484c73) Thanks [@eoinest](https://github.com/eoinest)! - Add browser SPA initialization and synchronous module-level `t()` translations
23
+ to gt-vue.
24
+ - Updated dependencies [[`f2204b9`](https://github.com/generaltranslation/gt/commit/f2204b990278865c54d14d337392930ea1ec31bf), [`b05b470`](https://github.com/generaltranslation/gt/commit/b05b4703fbbfd34a3cbee335c786e2e606346167), [`8d376e2`](https://github.com/generaltranslation/gt/commit/8d376e23c80828dfd8756e2f0fbf0f7725e0f178)]:
25
+ - @generaltranslation/format@0.1.8
26
+ - generaltranslation@9.1.7
27
+ - gt-i18n@1.0.17
28
+
29
+ ## 0.1.0-iris.2
30
+
31
+ ### Patch Changes
32
+
33
+ - 95b48df: Add browser SPA initialization and synchronous module-level `t()` translations
34
+ to gt-vue.
35
+
3
36
  ## 0.1.0-iris.1
4
37
 
5
38
  ### Patch Changes
package/README.md CHANGED
@@ -84,6 +84,90 @@ const setLocale = useSetLocale();
84
84
  `$context`; braces are literal text and no ICU formatting or interpolation is
85
85
  applied.
86
86
 
87
+ ## Module-level translations in a Vite SPA
88
+
89
+ Browser-only SPAs can call `t()` at module scope after `initializeGTSPA()` has
90
+ loaded the active locale. Use an async bootstrap function, and dynamically
91
+ import the rest of the application only after initialization.
92
+ This complements rather than replaces the `gt()` callback from `useGT()`,
93
+ which remains the normal API inside Vue components and for SSR applications.
94
+
95
+ ```ts
96
+ // src/index.ts
97
+ import { initializeGTSPA } from 'gt-vue';
98
+ import gtConfig from '../gt.config.json';
99
+ import loadTranslations from './loadTranslations';
100
+
101
+ async function bootstrap() {
102
+ const gt = await initializeGTSPA({ ...gtConfig, loadTranslations });
103
+ const { mount } = await import('./main');
104
+ mount(gt);
105
+ }
106
+
107
+ void bootstrap().catch((error: unknown) => {
108
+ console.error(error);
109
+ });
110
+ ```
111
+
112
+ Configure the CLI output and the Vite loader to use the same directory:
113
+
114
+ ```json
115
+ {
116
+ "defaultLocale": "en",
117
+ "locales": ["fr"],
118
+ "files": {
119
+ "gt": {
120
+ "output": "src/_gt/[locale].json"
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ ```ts
127
+ // src/loadTranslations.ts
128
+ export default async function loadTranslations(locale: string) {
129
+ const translations = await import(`./_gt/${locale}.json`);
130
+ return translations.default;
131
+ }
132
+ ```
133
+
134
+ Create an empty JSON file for each configured target locale before the first
135
+ translation run (for example, `src/_gt/fr.json` containing `{}`). The default
136
+ locale uses source content and does not need a loader file.
137
+
138
+ ```ts
139
+ // src/main.ts
140
+ import { createApp } from 'vue';
141
+ import type { GTPlugin } from 'gt-vue';
142
+ import App from './App.vue';
143
+
144
+ export function mount(gt: GTPlugin) {
145
+ createApp(App).use(gt).mount('#app');
146
+ }
147
+ ```
148
+
149
+ Install the plugin returned by `initializeGTSPA()` rather than creating a
150
+ second plugin. The returned instance is already preloaded and is the exact
151
+ runtime used by `t()`.
152
+
153
+ ```ts
154
+ // src/navigation.ts (loaded by the dynamic application import)
155
+ import { t } from 'gt-vue';
156
+
157
+ export const navigation = [
158
+ t('Documentation', { $context: 'primary navigation' }),
159
+ ];
160
+ ```
161
+
162
+ Like `useGT()`, `t()` supports only plain STRING content and static `$context`.
163
+ It does not support ICU syntax, interpolation, tagged templates, `$format`, or
164
+ `$maxChars`. The extractor registers static `t()` calls in the catalog.
165
+
166
+ SPA locale changes write the locale cookie and reload the page. Reloading is
167
+ intentional: it lets every module-level translation execute again after the new
168
+ locale catalog is preloaded. `initializeGTSPA()` and `t()` are not valid in SSR;
169
+ use one request-scoped `createGT({ locale })` instance there.
170
+
87
171
  Statically authored default-slot content inside a custom component participates
88
172
  in the surrounding translation. The component itself, its props, and its
89
173
  listeners are preserved while the translated content replaces its default
@@ -173,12 +257,18 @@ from `createGT()`, that cookie wins over `defaultLocale`. Use
173
257
  `localeCookieName` to share a different cookie with your routing or server
174
258
  integration.
175
259
 
176
- `setLocale()` loads a missing catalog before writing the cookie and rerendering
177
- consumers. A failed or superseded request leaves both the cookie and rendered
178
- locale unchanged. Direct changes to `document.cookie` are reflected by
179
- `plugin.getLocale()` and the next Vue render, but browsers do not emit cookie
180
- change events, so they do not schedule a render by themselves. Use gt-vue's
181
- setter for reactive locale changes.
260
+ For plugins created with `createGT()`, `setLocale()` loads a missing catalog
261
+ before writing the cookie and rerendering consumers. A failed or superseded
262
+ request leaves both the cookie and rendered locale unchanged. Direct changes to
263
+ `document.cookie` are reflected by `plugin.getLocale()` and the next Vue render,
264
+ but browsers do not emit cookie change events, so they do not schedule a render
265
+ by themselves. Use gt-vue's setter for reactive locale changes.
266
+
267
+ `initializeGTSPA()` instead pins the preloaded locale for the lifetime of the
268
+ page. Its `setLocale()` writes the cookie and reloads the document so
269
+ module-level `t()` calls execute again with the new catalog. Direct cookie
270
+ changes do not change the mounted SPA; they are resolved during the next page
271
+ initialization, and unsupported locales fall back to `defaultLocale`.
182
272
 
183
273
  For SSR, resolve the request locale on the server and pass it as
184
274
  `createGT({ locale })`. An explicit locale wins over a stale browser cookie,
package/dist/index.cjs CHANGED
@@ -6,37 +6,48 @@ let generaltranslation_types = require("generaltranslation/types");
6
6
  let generaltranslation_id = require("generaltranslation/id");
7
7
  let gt_i18n_internal_string = require("gt-i18n/internal/string");
8
8
  //#region src/runtime/localeCookie.ts
9
- function createCookieBackedLocale({ defaultLocale, locale: explicitLocale, localeCookieName }) {
9
+ function createCookieBackedLocale({ defaultLocale, locale: explicitLocale, localeCookieName, resolveLocale = identityLocale$1 }) {
10
10
  const cookieLocale = (0, gt_i18n_internal_cookies.getBrowserCookieValue)(localeCookieName);
11
- const resolvedLocale = explicitLocale ?? (cookieLocale || defaultLocale);
12
- const browserFallbackLocale = explicitLocale ?? defaultLocale;
11
+ const resolvedLocale = resolveLocale(explicitLocale ?? (cookieLocale || defaultLocale));
12
+ const browserFallbackLocale = resolveLocale(explicitLocale ?? defaultLocale);
13
13
  let serverLocale = resolvedLocale;
14
14
  (0, gt_i18n_internal_cookies.setBrowserCookieValue)(localeCookieName, resolvedLocale);
15
15
  return {
16
16
  getLocale() {
17
- if (typeof document !== "undefined") return (0, gt_i18n_internal_cookies.getBrowserCookieValue)(localeCookieName) || browserFallbackLocale;
17
+ if (typeof document !== "undefined") return resolveLocale((0, gt_i18n_internal_cookies.getBrowserCookieValue)(localeCookieName) || browserFallbackLocale);
18
18
  return serverLocale;
19
19
  },
20
20
  setLocale(nextLocale) {
21
- serverLocale = nextLocale;
22
- (0, gt_i18n_internal_cookies.setBrowserCookieValue)(localeCookieName, nextLocale);
21
+ const resolvedNextLocale = resolveLocale(nextLocale);
22
+ serverLocale = resolvedNextLocale;
23
+ (0, gt_i18n_internal_cookies.setBrowserCookieValue)(localeCookieName, resolvedNextLocale);
23
24
  }
24
25
  };
25
26
  }
27
+ function identityLocale$1(locale) {
28
+ return locale;
29
+ }
26
30
  //#endregion
27
31
  //#region src/runtime/state.ts
28
- const gtContextKey = Symbol("gt-vue");
29
- function createGT({ defaultLocale = generaltranslation_internal.libraryDefaultLocale, loadTranslations, locale: explicitLocale, localeCookieName = gt_i18n_internal_cookies.defaultLocaleCookieName } = {}) {
32
+ const gtContextKey = Symbol.for("generaltranslation.gt-vue.context");
33
+ function createGT(options = {}) {
34
+ return createGTRuntime(options).plugin;
35
+ }
36
+ function createGTRuntime({ defaultLocale = generaltranslation_internal.libraryDefaultLocale, loadTranslations, locale: explicitLocale, localeCookieName = gt_i18n_internal_cookies.defaultLocaleCookieName } = {}, runtimeOptions = {}) {
30
37
  const localeAccessor = createCookieBackedLocale({
31
38
  defaultLocale,
32
39
  locale: explicitLocale,
33
- localeCookieName
40
+ localeCookieName,
41
+ resolveLocale: runtimeOptions.resolveLocale
34
42
  });
35
43
  const revision = (0, vue.ref)(0);
36
44
  const catalogs = new Map([[defaultLocale, {}]]);
37
45
  const pending = /* @__PURE__ */ new Map();
46
+ const initialLocale = localeAccessor.getLocale();
47
+ const getLocale = runtimeOptions.pinLocale ? () => initialLocale : () => localeAccessor.getLocale();
38
48
  let localeRequest = 0;
39
- const load = async (targetLocale) => {
49
+ const load = async (locale) => {
50
+ const targetLocale = runtimeOptions.resolveLocale?.(locale) ?? locale;
40
51
  const cached = catalogs.get(targetLocale);
41
52
  if (cached) return cached;
42
53
  const currentPending = pending.get(targetLocale);
@@ -60,36 +71,42 @@ function createGT({ defaultLocale = generaltranslation_internal.libraryDefaultLo
60
71
  pending.set(targetLocale, promise);
61
72
  return promise;
62
73
  };
63
- const setLocale = async (targetLocale) => {
74
+ const setLocale = runtimeOptions.reloadDocument ? async (targetLocale) => {
75
+ localeAccessor.setLocale(targetLocale);
76
+ runtimeOptions.reloadDocument?.();
77
+ } : async (targetLocale) => {
64
78
  const request = ++localeRequest;
65
79
  await load(targetLocale);
66
80
  if (request !== localeRequest) return;
67
81
  localeAccessor.setLocale(targetLocale);
68
82
  revision.value += 1;
69
83
  };
70
- const getLocale = () => {
71
- revision.value;
72
- return localeAccessor.getLocale();
73
- };
74
84
  const state = {
75
85
  defaultLocale,
76
86
  getCatalog() {
77
87
  revision.value;
78
88
  return catalogs.get(getLocale()) ?? {};
79
89
  },
80
- getLocale,
90
+ getLocale() {
91
+ revision.value;
92
+ return getLocale();
93
+ },
81
94
  loadTranslations: load,
95
+ resolveFormattingLocale: runtimeOptions.resolveFormattingLocale,
82
96
  revision,
83
97
  setLocale
84
98
  };
85
99
  return {
86
- getLocale,
87
- install(app) {
88
- app.provide(gtContextKey, state);
89
- load(getLocale()).catch(() => void 0);
100
+ plugin: {
101
+ getLocale: state.getLocale,
102
+ install(app) {
103
+ app.provide(gtContextKey, state);
104
+ load(state.getLocale()).catch(() => void 0);
105
+ },
106
+ loadTranslations: load,
107
+ setLocale
90
108
  },
91
- loadTranslations: load,
92
- setLocale
109
+ state
93
110
  };
94
111
  }
95
112
  function useGTState() {
@@ -99,7 +116,7 @@ function useGTState() {
99
116
  source: "gt-vue",
100
117
  severity: "Error",
101
118
  whatHappened: "The GT Vue plugin is not installed",
102
- fix: "Install the plugin with app.use(createGT(options))"
119
+ fix: "Install the exact plugin returned by initializeGTSPA() with app.use(plugin), or install app.use(createGT(options)) for non-SPA usage"
103
120
  }));
104
121
  }
105
122
  //#endregion
@@ -120,13 +137,18 @@ const NON_BRANCH_ATTRIBUTE_NAMES = new Set([
120
137
  function withGTMetadata(component, metadata) {
121
138
  return Object.assign(component, { _gtt: metadata });
122
139
  }
123
- function getFormatLocales(locales, locale, defaultLocale = generaltranslation_internal.libraryDefaultLocale) {
124
- if (locale === defaultLocale) return [defaultLocale];
140
+ function getFormatLocales(locales, locale, defaultLocale = generaltranslation_internal.libraryDefaultLocale, resolveLocale = identityLocale) {
141
+ const resolvedLocale = resolveLocale(locale);
142
+ const resolvedDefaultLocale = resolveLocale(defaultLocale);
143
+ if (resolvedLocale === resolvedDefaultLocale) return [resolvedDefaultLocale];
125
144
  return [...new Set([
126
145
  ...locales ?? [],
127
146
  locale,
128
147
  defaultLocale
129
- ])];
148
+ ].map(resolveLocale))];
149
+ }
150
+ function identityLocale(locale) {
151
+ return locale;
130
152
  }
131
153
  function asFragmentRoot(children) {
132
154
  if (Array.isArray(children)) return children;
@@ -162,7 +184,7 @@ const Plural = /* @__PURE__ */ withGTMetadata(/* @__PURE__ */ (0, vue.defineComp
162
184
  const state = useGTState();
163
185
  return () => {
164
186
  const branches = getBranchNames(attrs, slots).filter(generaltranslation_internal.isAcceptedPluralForm);
165
- const branch = (0, generaltranslation_internal.getPluralForm)(props.n, branches, getFormatLocales(props.locales, state.getLocale(), state.defaultLocale));
187
+ const branch = (0, generaltranslation_internal.getPluralForm)(props.n, branches, getFormatLocales(props.locales, state.getLocale(), state.defaultLocale, state.resolveFormattingLocale));
166
188
  const namedSlot = branch && slots[branch];
167
189
  return asFragmentRoot(branch && attrs[branch] === null && typeof namedSlot !== "function" ? slots.default?.() ?? null : getBranchContent(branch, attrs, slots));
168
190
  };
@@ -637,7 +659,7 @@ function getBranchKey(source) {
637
659
  function getPluralKey(n, branches, state, locale = state.getLocale()) {
638
660
  const forms = branches.filter(generaltranslation_internal.isAcceptedPluralForm);
639
661
  if (!forms.length) return void 0;
640
- return (0, generaltranslation_internal.getPluralForm)(n, forms, getFormatLocales(void 0, locale, state.defaultLocale)) || void 0;
662
+ return (0, generaltranslation_internal.getPluralForm)(n, forms, getFormatLocales(void 0, locale, state.defaultLocale, state.resolveFormattingLocale)) || void 0;
641
663
  }
642
664
  function getSelectedSourceBranch(source, branch) {
643
665
  if (!branch || !Object.hasOwn(source.branches, branch)) return {
@@ -885,7 +907,7 @@ const Currency = /* @__PURE__ */ withGTMetadata(/* @__PURE__ */ (0, vue.defineCo
885
907
  }
886
908
  }), "variable-currency");
887
909
  function getVariableFormatLocales(props, state) {
888
- return props._locale === void 0 ? getFormatLocales(props.locales, state.getLocale(), state.defaultLocale) : getFormatLocales(void 0, props._locale, state.defaultLocale);
910
+ return props._locale === void 0 ? getFormatLocales(props.locales, state.getLocale(), state.defaultLocale, state.resolveFormattingLocale) : getFormatLocales(void 0, props._locale, state.defaultLocale, state.resolveFormattingLocale);
889
911
  }
890
912
  //#endregion
891
913
  //#region src/composables/locale.ts
@@ -926,6 +948,95 @@ function msg(message, options) {
926
948
  return (0, gt_i18n_internal_string.msgString)(message, options);
927
949
  }
928
950
  //#endregion
951
+ //#region src/runtime/spa.ts
952
+ async function initializeGTSPA(options = {}) {
953
+ assertBrowser(createInitializeOnServerError);
954
+ const manager = getSPARuntimeManager();
955
+ if (manager.runtime) return manager.runtime.plugin;
956
+ if (!manager.initialization) manager.initialization = createSPARuntime(options);
957
+ const initialization = manager.initialization;
958
+ try {
959
+ const runtime = await initialization;
960
+ if (manager.initialization === initialization) manager.runtime = runtime;
961
+ return runtime.plugin;
962
+ } finally {
963
+ if (manager.initialization === initialization) manager.initialization = void 0;
964
+ }
965
+ }
966
+ const t = (message, options = {}) => {
967
+ assertBrowser(createTranslateOnServerError);
968
+ const runtime = getSPARuntimeManager().runtime;
969
+ if (!runtime) throw new Error(createTranslateBeforeInitializationError());
970
+ return translateString(runtime.state, message, options);
971
+ };
972
+ async function createSPARuntime(options) {
973
+ const { customMapping, locales, ...createOptions } = options;
974
+ const { determineLocale, resolveCanonicalLocale } = await import("@generaltranslation/format");
975
+ const resolveFormattingLocale = (locale) => {
976
+ const canonicalLocale = resolveCanonicalLocale(locale, customMapping);
977
+ return determineLocale(canonicalLocale, [canonicalLocale]) ?? canonicalLocale;
978
+ };
979
+ const runtime = createGTRuntime(createOptions, {
980
+ pinLocale: true,
981
+ reloadDocument: () => window.location.reload(),
982
+ resolveFormattingLocale,
983
+ resolveLocale: createSupportedLocaleResolver(createOptions.defaultLocale ?? generaltranslation_internal.libraryDefaultLocale, locales, determineLocale, resolveFormattingLocale)
984
+ });
985
+ await runtime.plugin.loadTranslations(runtime.plugin.getLocale());
986
+ return runtime;
987
+ }
988
+ function createSupportedLocaleResolver(defaultLocale, locales, determineLocale, canonicalizeLocale) {
989
+ if (locales === void 0) {
990
+ const canonicalDefaultLocale = canonicalizeLocale(defaultLocale);
991
+ return (locale) => locale.toLowerCase() === defaultLocale.toLowerCase() || canonicalizeLocale(locale) === canonicalDefaultLocale ? defaultLocale : locale;
992
+ }
993
+ const configuredLocales = Array.from(new Set([defaultLocale, ...locales]));
994
+ const canonicalLocales = configuredLocales.map(canonicalizeLocale);
995
+ return (locale) => {
996
+ const exactIndex = configuredLocales.findIndex((configuredLocale) => configuredLocale.toLowerCase() === locale.toLowerCase());
997
+ if (exactIndex !== -1) return configuredLocales[exactIndex];
998
+ const matchedLocale = determineLocale(canonicalizeLocale(locale), canonicalLocales);
999
+ if (matchedLocale === void 0) return defaultLocale;
1000
+ return configuredLocales[canonicalLocales.indexOf(matchedLocale)] ?? defaultLocale;
1001
+ };
1002
+ }
1003
+ function getSPARuntimeManager() {
1004
+ const globalObject = globalThis;
1005
+ const registry = globalObject.__generaltranslation ??= {};
1006
+ const vueNamespace = registry.vue ??= {};
1007
+ return vueNamespace.spaRuntime ??= {};
1008
+ }
1009
+ function assertBrowser(createDiagnostic) {
1010
+ if (typeof window === "undefined" || typeof document === "undefined") throw new Error(createDiagnostic());
1011
+ }
1012
+ function createInitializeOnServerError() {
1013
+ return (0, generaltranslation_internal.createDiagnosticMessage)({
1014
+ source: "gt-vue",
1015
+ severity: "Error",
1016
+ whatHappened: "initializeGTSPA() cannot run in a server-rendered environment",
1017
+ why: "it creates browser-global translation state and changes locales by reloading the document",
1018
+ fix: "Use createGT({ locale }) once per server request and preload that request locale before rendering"
1019
+ });
1020
+ }
1021
+ function createTranslateOnServerError() {
1022
+ return (0, generaltranslation_internal.createDiagnosticMessage)({
1023
+ source: "gt-vue",
1024
+ severity: "Error",
1025
+ whatHappened: "t() cannot run in a server-rendered environment",
1026
+ why: "module-level translation state cannot be safely shared across server requests",
1027
+ fix: "Use request-scoped createGT({ locale }) with useGT() inside Vue components, or register module strings with msg() and resolve them through useMessages()"
1028
+ });
1029
+ }
1030
+ function createTranslateBeforeInitializationError() {
1031
+ return (0, generaltranslation_internal.createDiagnosticMessage)({
1032
+ source: "gt-vue",
1033
+ severity: "Error",
1034
+ whatHappened: "t() ran before the GT Vue SPA runtime finished initializing",
1035
+ why: "module-level translations require the active locale catalog to be loaded first",
1036
+ fix: "Await initializeGTSPA() in a bootstrap module, then dynamically import the application entry point"
1037
+ });
1038
+ }
1039
+ //#endregion
929
1040
  exports.Branch = Branch;
930
1041
  exports.Currency = Currency;
931
1042
  exports.DateTime = DateTime;
@@ -934,7 +1045,9 @@ exports.Plural = Plural;
934
1045
  exports.T = T;
935
1046
  exports.Var = Var;
936
1047
  exports.createGT = createGT;
1048
+ exports.initializeGTSPA = initializeGTSPA;
937
1049
  exports.msg = msg;
1050
+ exports.t = t;
938
1051
  exports.useGT = useGT;
939
1052
  exports.useLocale = useLocale;
940
1053
  exports.useMessages = useMessages;