@lokascript/i18n 2.9.4 → 2.10.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.
@@ -145,7 +145,7 @@ interface KeywordProviderOptions {
145
145
  * - Each role has distinct grammatical markers per language (see profiles/index.ts)
146
146
  * - Usage percentages based on pattern database analysis
147
147
  */
148
- type SemanticRole = 'action' | 'agent' | 'patient' | 'source' | 'destination' | 'goal' | 'event' | 'condition' | 'quantity' | 'duration' | 'responseType' | 'method' | 'style' | 'manner' | 'loopType' | 'continues';
148
+ type SemanticRole = 'action' | 'agent' | 'patient' | 'source' | 'destination' | 'goal' | 'recipient' | 'event' | 'condition' | 'quantity' | 'duration' | 'responseType' | 'method' | 'style' | 'manner' | 'loopType' | 'continues';
149
149
  /**
150
150
  * Word order patterns
151
151
  * These represent the major typological categories
@@ -907,6 +907,23 @@ declare class GrammarTransformer {
907
907
  * Transform a single hyperscript statement (no compound "then" chains).
908
908
  */
909
909
  private transformSingle;
910
+ /**
911
+ * Clause carrying a masked `using view transition` tail: strip the opaque
912
+ * token, transform the clause alone, and re-append the token at the very end.
913
+ *
914
+ * The tail is a clause-final modifier in every word order the corpus emits:
915
+ * the semantic side matches it as the literal `using view` marker plus a value
916
+ * word, and the SOV/VSO event-handler patterns admit it as an optional
917
+ * TRAILING group (after the with-marked operand). So the target position is
918
+ * "end of the transformed clause" for all 24 languages — no per-profile
919
+ * placement decision, which is what makes this a passthrough rather than a
920
+ * role.
921
+ *
922
+ * Returns null when the clause carries no masked tail, or when the token is
923
+ * not clause-final (nothing to reposition — leaving it in place still restores
924
+ * verbatim English).
925
+ */
926
+ private transformWithViewTransitionTail;
910
927
  /**
911
928
  * `<command …> end` fragments: transform the command without its stranded
912
929
  * terminator, then re-append the translated terminator as a standalone
@@ -145,7 +145,7 @@ interface KeywordProviderOptions {
145
145
  * - Each role has distinct grammatical markers per language (see profiles/index.ts)
146
146
  * - Usage percentages based on pattern database analysis
147
147
  */
148
- type SemanticRole = 'action' | 'agent' | 'patient' | 'source' | 'destination' | 'goal' | 'event' | 'condition' | 'quantity' | 'duration' | 'responseType' | 'method' | 'style' | 'manner' | 'loopType' | 'continues';
148
+ type SemanticRole = 'action' | 'agent' | 'patient' | 'source' | 'destination' | 'goal' | 'recipient' | 'event' | 'condition' | 'quantity' | 'duration' | 'responseType' | 'method' | 'style' | 'manner' | 'loopType' | 'continues';
149
149
  /**
150
150
  * Word order patterns
151
151
  * These represent the major typological categories
@@ -907,6 +907,23 @@ declare class GrammarTransformer {
907
907
  * Transform a single hyperscript statement (no compound "then" chains).
908
908
  */
909
909
  private transformSingle;
910
+ /**
911
+ * Clause carrying a masked `using view transition` tail: strip the opaque
912
+ * token, transform the clause alone, and re-append the token at the very end.
913
+ *
914
+ * The tail is a clause-final modifier in every word order the corpus emits:
915
+ * the semantic side matches it as the literal `using view` marker plus a value
916
+ * word, and the SOV/VSO event-handler patterns admit it as an optional
917
+ * TRAILING group (after the with-marked operand). So the target position is
918
+ * "end of the transformed clause" for all 24 languages — no per-profile
919
+ * placement decision, which is what makes this a passthrough rather than a
920
+ * role.
921
+ *
922
+ * Returns null when the clause carries no masked tail, or when the token is
923
+ * not clause-final (nothing to reposition — leaving it in place still restores
924
+ * verbatim English).
925
+ */
926
+ private transformWithViewTransitionTail;
910
927
  /**
911
928
  * `<command …> end` fragments: transform the command without its stranded
912
929
  * terminator, then re-append the translated terminator as a standalone
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokascript/i18n",
3
- "version": "2.9.4",
3
+ "version": "2.10.0",
4
4
  "description": "Grammar transformation and keyword translation for hyperscript",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -64,8 +64,8 @@
64
64
  },
65
65
  "homepage": "https://github.com/codetalcott/hyperfixi/tree/main/packages/i18n#readme",
66
66
  "dependencies": {
67
- "@hyperfixi/core": "^2.9.4",
68
- "@lokascript/semantic": "^2.9.4",
67
+ "@hyperfixi/core": "^2.10.0",
68
+ "@lokascript/semantic": "^2.10.0",
69
69
  "esbuild": "^0.28.0",
70
70
  "happy-dom": "^20.10.6",
71
71
  "vite": "^8.1.4"
@@ -2662,3 +2662,90 @@ describe('SOV if-blocks get a then-connective before a positional branch head (F
2662
2662
  expect(out).toContain('llulla "Invalid form" ta qillqakuy');
2663
2663
  });
2664
2664
  });
2665
+
2666
+ // `swap #a with #b using view transition` — the tail the transformer had never
2667
+ // seen. `using` is in no dictionary and no role table, so it swept into whatever
2668
+ // role phrase was open; `transition` IS a translated command keyword in every
2669
+ // dictionary AND is in ENGLISH_COMMANDS, so splitOnCommandBoundaries cut the
2670
+ // clause there and the rejoin planted a phantom translated `transition` COMMAND
2671
+ // after the target's then-connective (`intercambiar #a con #b using view
2672
+ // entonces transición`). The phrase has no native form in any of the 24
2673
+ // languages — semantic matches the literal English `using view` marker
2674
+ // everywhere (USING_VIEW_MARKER_ALL_LANGS) — so it is masked before any
2675
+ // splitting/translation and re-appended verbatim at the clause tail, which is
2676
+ // where every word order's patterns admit it.
2677
+ describe('`using view transition` is a passthrough tail, not translated content', () => {
2678
+ const SRC = 'on click swap #a with #b using view transition';
2679
+
2680
+ // One per word-order family: SVO Romance/Germanic/Slavic, V2, SOV
2681
+ // particle/agglutinative, VSO, and the two object-marking SVO languages.
2682
+ const TRANSLATED_TRANSITION: Array<[string, string]> = [
2683
+ ['es', 'transición'],
2684
+ ['fr', 'transition'], // fr's own verb form — the passthrough must not re-emit it as a command
2685
+ ['de', 'übergang'],
2686
+ ['ja', '遷移'],
2687
+ ['ko', '전환'],
2688
+ ['tr', 'geçiş'],
2689
+ ['qu', 'pasay'],
2690
+ ['bn', 'সংক্রমণ'],
2691
+ ['hi', 'संक्रमण'],
2692
+ ['ar', 'انتقال'],
2693
+ ['zh', '过渡'],
2694
+ ['he', 'מעבר'],
2695
+ ['ru', 'анимировать'],
2696
+ ['th', 'เปลี่ยนผ่าน'],
2697
+ ['vi', 'chuyển tiếp'],
2698
+ ['tl', 'transisyon'],
2699
+ ];
2700
+
2701
+ for (const [lang, translated] of TRANSLATED_TRANSITION) {
2702
+ it(`[${lang}] emits the English tail verbatim at the clause end`, () => {
2703
+ const out = new GrammarTransformer('en', lang).transform(SRC);
2704
+ expect(out).toMatch(/using view transition$/);
2705
+ // No phantom then-connective, and no translated `transition` command.
2706
+ if (translated !== 'transition') {
2707
+ expect(out).not.toContain(translated);
2708
+ }
2709
+ });
2710
+ }
2711
+
2712
+ it('the tail does not displace the clause it modifies (both operands survive)', () => {
2713
+ for (const lang of ['es', 'ja', 'ar', 'tr', 'zh', 'qu']) {
2714
+ const out = new GrammarTransformer('en', lang).transform(SRC);
2715
+ expect(out, lang).toContain('#a');
2716
+ expect(out, lang).toContain('#b');
2717
+ }
2718
+ });
2719
+
2720
+ it('`transition` as a real command keyword still translates (both-ways negative)', () => {
2721
+ // The mask must be anchored on the literal `using view` phrase, not on the
2722
+ // word `transition` — the standalone transition command has to keep its
2723
+ // dictionary form or every animate row would render English.
2724
+ const es = new GrammarTransformer('en', 'es').transform(
2725
+ 'on click transition opacity to 0 over 300ms'
2726
+ );
2727
+ expect(es).toContain('transición');
2728
+ const ja = new GrammarTransformer('en', 'ja').transform(
2729
+ 'on click transition opacity to 0 over 300ms'
2730
+ );
2731
+ expect(ja).toContain('遷移');
2732
+ });
2733
+
2734
+ it('a `then`-chained clause after the tail still splits (the tail never swallows a boundary)', () => {
2735
+ const out = new GrammarTransformer('en', 'es').transform(
2736
+ 'on click swap #a with #b using view transition then log "done"'
2737
+ );
2738
+ expect(out).toContain('using view transition');
2739
+ expect(out).toContain('entonces');
2740
+ expect(out).toContain('"done"');
2741
+ });
2742
+
2743
+ it('`process partials in it using view transition` keeps its tail too', () => {
2744
+ for (const lang of ['es', 'ja', 'tr']) {
2745
+ const out = new GrammarTransformer('en', lang).transform(
2746
+ 'process partials in it using view transition'
2747
+ );
2748
+ expect(out, lang).toMatch(/using view transition$/);
2749
+ }
2750
+ });
2751
+ });
@@ -1778,6 +1778,59 @@ function restoreCaretScopes(input: string, scopes: string[]): string {
1778
1778
  );
1779
1779
  }
1780
1780
 
1781
+ // =============================================================================
1782
+ // View-transition tail masking (`using view transition`)
1783
+ // =============================================================================
1784
+
1785
+ /** Private-use sentinels bracketing a masked view-transition-tail index. */
1786
+ const VIEW_TAIL_OPEN = '\uE002';
1787
+ const VIEW_TAIL_CLOSE = '\uE003';
1788
+
1789
+ /**
1790
+ * Match `swap`/`process`'s trailing `using view transition` modifier.
1791
+ *
1792
+ * `using` is in no dictionary and in no role table, so it sweeps into whatever
1793
+ * role phrase is open; `transition` IS a translated command keyword in every
1794
+ * dictionary (es `transición`, de `übergang`, ja `遷移`) and is in
1795
+ * `ENGLISH_COMMANDS`, so `splitOnCommandBoundaries` splits the clause there and
1796
+ * the rejoin plants a phantom translated `transition` COMMAND after the target's
1797
+ * `then`-connective (`intercambiar #a con #b using view entonces transición`).
1798
+ *
1799
+ * The phrase has no native form in any of the 24 languages — semantic's
1800
+ * `USING_VIEW_MARKER_ALL_LANGS` matches the literal English `using view` marker
1801
+ * everywhere — so it is a passthrough, matched on the English surface regardless
1802
+ * of source locale. The value word is optional so a bare `using view` still
1803
+ * masks rather than half-splitting; `then` is excluded so a clause boundary is
1804
+ * never swallowed into the tail.
1805
+ */
1806
+ const VIEW_TAIL_RE = /\busing\s+view\b(?:\s+(?!then\b)[A-Za-z][\w-]*)?/gi;
1807
+
1808
+ /** Whether a token is a masked view-transition tail. */
1809
+ const VIEW_TAIL_TOKEN_RE = new RegExp(`^${VIEW_TAIL_OPEN}(\\d+)${VIEW_TAIL_CLOSE}$`);
1810
+
1811
+ /**
1812
+ * Mask every `using view <value>` tail behind an opaque token, so the phrase
1813
+ * never reaches the splitter, the word translator, or the role parser. Returns
1814
+ * null when there's nothing to mask.
1815
+ */
1816
+ function maskViewTransitionTails(input: string): { masked: string; tails: string[] } | null {
1817
+ const tails: string[] = [];
1818
+ const masked = input.replace(VIEW_TAIL_RE, match => {
1819
+ const idx = tails.length;
1820
+ tails.push(match);
1821
+ return `${VIEW_TAIL_OPEN}${idx}${VIEW_TAIL_CLOSE}`;
1822
+ });
1823
+ return tails.length > 0 ? { masked, tails } : null;
1824
+ }
1825
+
1826
+ /** Restore masked view-transition tokens to their verbatim English phrase. */
1827
+ function restoreViewTransitionTails(input: string, tails: string[]): string {
1828
+ return input.replace(
1829
+ new RegExp(`${VIEW_TAIL_OPEN}(\\d+)${VIEW_TAIL_CLOSE}`, 'g'),
1830
+ (_m, n: string) => tails[Number(n)] ?? ''
1831
+ );
1832
+ }
1833
+
1781
1834
  // =============================================================================
1782
1835
  // Main Transformer
1783
1836
  // =============================================================================
@@ -1813,6 +1866,17 @@ export class GrammarTransformer {
1813
1866
  }
1814
1867
 
1815
1868
  private transformInternal(input: string): string {
1869
+ // `using view transition` is a passthrough phrase, not translatable content:
1870
+ // `using` is in no dictionary and `transition` is a COMMAND keyword, so left
1871
+ // in place the splitter tears the clause apart there and the tail renders as
1872
+ // a phantom translated command. Mask it before any splitting/translation and
1873
+ // restore it verbatim; transformSingle re-appends the opaque token at the
1874
+ // clause tail so it lands after the reorder, not inside a role phrase.
1875
+ const viewTails = maskViewTransitionTails(input);
1876
+ if (viewTails) {
1877
+ return restoreViewTransitionTails(this.transformInternal(viewTails.masked), viewTails.tails);
1878
+ }
1879
+
1816
1880
  // Caret-scoped variable reads (`^name on <selector>`) carry a second,
1817
1881
  // overloaded `on` that the splitter/event-parser would mistake for an event
1818
1882
  // or command boundary — mangling `put ^count on #host into me`. Mask the
@@ -1921,6 +1985,18 @@ export class GrammarTransformer {
1921
1985
  return setScope;
1922
1986
  }
1923
1987
 
1988
+ // 0c. Masked `using view transition` tail (see maskViewTransitionTails):
1989
+ // strip the opaque token, transform the clause without it, and re-append
1990
+ // it at the clause tail. Left in the token stream it would be swept into
1991
+ // the open role's VALUE and rendered ahead of that role's marker
1992
+ // (ja `#b using view transition で` instead of `#b で using view
1993
+ // transition`) — the same failure mode transformWithTrailingEnd fixes
1994
+ // for a stranded `end`.
1995
+ const viewTail = this.transformWithViewTransitionTail(input);
1996
+ if (viewTail !== null) {
1997
+ return viewTail;
1998
+ }
1999
+
1924
2000
  // 1. Parse into semantic roles
1925
2001
  const parsed = parseStatement(input, this.sourceProfile.code);
1926
2002
  if (!parsed) {
@@ -1963,6 +2039,36 @@ export class GrammarTransformer {
1963
2039
  return joinTokens(reordered.map(e => e.translated || e.value));
1964
2040
  }
1965
2041
 
2042
+ /**
2043
+ * Clause carrying a masked `using view transition` tail: strip the opaque
2044
+ * token, transform the clause alone, and re-append the token at the very end.
2045
+ *
2046
+ * The tail is a clause-final modifier in every word order the corpus emits:
2047
+ * the semantic side matches it as the literal `using view` marker plus a value
2048
+ * word, and the SOV/VSO event-handler patterns admit it as an optional
2049
+ * TRAILING group (after the with-marked operand). So the target position is
2050
+ * "end of the transformed clause" for all 24 languages — no per-profile
2051
+ * placement decision, which is what makes this a passthrough rather than a
2052
+ * role.
2053
+ *
2054
+ * Returns null when the clause carries no masked tail, or when the token is
2055
+ * not clause-final (nothing to reposition — leaving it in place still restores
2056
+ * verbatim English).
2057
+ */
2058
+ private transformWithViewTransitionTail(input: string): string | null {
2059
+ const trimmed = input.trim();
2060
+ const tokens = trimmed.split(/\s+/);
2061
+ if (tokens.length < 2) {
2062
+ return null;
2063
+ }
2064
+ if (!VIEW_TAIL_TOKEN_RE.test(tokens[tokens.length - 1])) {
2065
+ return null;
2066
+ }
2067
+ const tail = tokens[tokens.length - 1];
2068
+ const head = tokens.slice(0, -1).join(' ');
2069
+ return `${this.transformSingle(head)} ${tail}`;
2070
+ }
2071
+
1966
2072
  /**
1967
2073
  * `<command …> end` fragments: transform the command without its stranded
1968
2074
  * terminator, then re-append the translated terminator as a standalone
@@ -66,6 +66,10 @@ export type SemanticRole =
66
66
  | 'source' // Origin (from #input, from URL)
67
67
  | 'destination' // Target location (into #output, to .class)
68
68
  | 'goal' // Target value/state (to 'red', to 100)
69
+ | 'recipient' // Who receives what the action transfers (take .active from .tab for me).
70
+ // Declared for parity with the semantic union; NO i18n profile marks it yet
71
+ // — en still maps `for` to duration lexically and suppresses the marker for
72
+ // pronoun values (see insertMarkers below).
69
73
  | 'event' // Trigger (click, input, keydown)
70
74
  | 'condition' // Boolean expression (if x > 5)
71
75
  // Quantitative roles