sugarcube-i18n 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/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # SugarCube i18n Plugin
2
+
3
+ A lightweight, open-source plugin to add multilingual support to [SugarCube 2.x](http://www.motoslave.net/sugarcube/2/) stories using [i18next](https://www.i18next.com/).
4
+
5
+ ## Features
6
+
7
+ - **Easy Translation**: Use the `<<t>>` macro to display text.
8
+ - **External Files**: Load translations from clean JSON files.
9
+ - **Persistence**: Automatically saves and restores the selected language on save/load.
10
+ - **Interpolation**: Support for variables inside translations (e.g., "Hello, {{player}}!").
11
+ - **Language Switching**: Hot-swap languages without reloading the page.
12
+
13
+
14
+ ## CLI Build (Tweego)
15
+
16
+ If you are using `tweego` from the command line, you must include the JavaScript file in your build command so it is bundled directly.
17
+
18
+ ```bash
19
+ tweego -o build/story.html story.twee sugarcube-i18next.js
20
+ ```
21
+
22
+ ## Installation
23
+
24
+
25
+ 1. **Download the Script**: Get `sugarcube-i18next.js` from this repository.
26
+ 2. **Add to Twine**:
27
+ - Open your story in Twine 2.
28
+ - Go to **Story Menu** -> **Edit Story JavaScript**.
29
+ - Paste the entire content of `sugarcube-i18next.js`.
30
+ 3. **External Library**: The script automatically loads `i18next` from a CDN (`unpkg.com`).
31
+ - *Note*: You need an internet connection for the CDN to work. For offline use, download `i18next.min.js` and add its content to your Story JavaScript *before* the plugin code.
32
+
33
+ ## Usage
34
+
35
+ ### 1. Create Translation Files
36
+ Create JSON files for each language (e.g., `en.json`, `es.json`) in a folder named `locales` relative to your HTML game file.
37
+
38
+ **locales/en.json**
39
+ ```json
40
+ {
41
+ "greeting": "Hello, {{name}}!",
42
+ "start_btn": "Start Game"
43
+ }
44
+ ```
45
+
46
+ **locales/es.json**
47
+ ```json
48
+ {
49
+ "greeting": "¡Hola, {{name}}!",
50
+ "start_btn": "Comenzar Juego"
51
+ }
52
+ ```
53
+
54
+ ### 2. Initialize in StoryInit
55
+ Load your translation files in the `StoryInit` passage.
56
+
57
+ ```
58
+ <<loadTranslations "locales/en.json" "en">>
59
+ <<loadTranslations "locales/es.json" "es">>
60
+ ```
61
+
62
+ > **Important**: Loading local JSON files via `fetch` requires a local web server due to browser security policies (CORS). If you just open the HTML file directly from disk, it might fail. Use VS Code Live Server or `python -m http.server` to test.
63
+
64
+ ### 3. Display Text
65
+ Use the `<<t>>` macro to translate text. You can pass interpolation options as an object or as key-value pairs.
66
+
67
+ ```
68
+ /* Option 1: Key-Value pairs (Recommended for simple cases) */
69
+ <<t "greeting" "name" $name>>
70
+
71
+ /* Option 2: Using an object variable */
72
+ <<set $opts to {name: $name}>>
73
+ <<t "greeting" $opts>>
74
+
75
+ /* Option 3: Basic usage */
76
+ <<t "welcome">>
77
+ ```
78
+
79
+ ### 4. Translated Links
80
+ Use `<<tlink>>` to simplify creating links with translated text.
81
+
82
+ ```
83
+ /* Basic Link */
84
+ <<tlink "start_btn" "NextPassage">>
85
+
86
+ /* Link with interpolation */
87
+ <<tlink "continue_btn" "Chapter1" "chapter" 1>>
88
+ ```
89
+
90
+ ### 5. Switch Language
91
+ Use macros or buttons to let the user change the language.
92
+
93
+ ```
94
+ <<button "English">>
95
+ <<setLang "en">>
96
+ <</button>>
97
+
98
+ <<button "Español">>
99
+ <<setLang "es">>
100
+ <</button>>
101
+ ```
102
+
103
+ ## API Reference
104
+
105
+ ### `<<loadTranslations "path" "langCode">>`
106
+ Loads a JSON file from the specified path and assigns it to the language code.
107
+ - **path**: Relative URL to the JSON file.
108
+ - **langCode**: The language code (e.g., 'en', 'es', 'fr').
109
+
110
+ ### `<<setLang "langCode">>`
111
+ Switches the current language to `langCode`.
112
+ - Updates `$lang` variable.
113
+ - Refreshes the current passage to apply changes immediately.
114
+
115
+ ### `<<t "key" [options]>>`
116
+ Returns the translation for the given key.
117
+ - **key**: The JSON key.
118
+ - **options**:
119
+ - **Object**: A JavaScript object (e.g., `<<set $o to {n:1}>> <<t "k" $o>>`).
120
+ - **Key-Value Pairs**: Alternating key and value arguments (e.g., `<<t "key" "param" $value>>`).
121
+
122
+ ### `<<tlink "key" "passage" [options]>>`
123
+ Creates a standard SugarCube link `<<link>>` using the translated text as the label.
124
+ - **key**: The JSON key for the link text.
125
+ - **passage**: The destination passage name.
126
+ - **options**: Same interpolation options as `<<t>>`.
127
+
128
+ ## Editor Support (Optional)
129
+
130
+ To avoid macro warnings in editors like Tweego / t3lt, you can add a
131
+ `twee-config.yaml` file defining the custom macros.
132
+
133
+ An example is provided in the `editor/` folder.
134
+
135
+
136
+ ## Troubleshooting
137
+
138
+ - **"Failed to load locales/en.json"**: This is usually a CORS error. Ensure you are running the game via a local server (http://localhost...) and not file protocol (file://...).
139
+ - **"i18next is not defined"**: Check your internet connection (if using CDN) or ensure the library logic is correct.
140
+ - **Missing Keys**: Use `debug: true` in the internal `i18nOptions` inside the JS file to see warnings in the browser console.
141
+
142
+ ## License
143
+ MIT License. Free to use in commercial and non-commercial projects.
@@ -0,0 +1,2 @@
1
+ const t="lang",e={debug:!0,fallbackLng:"en",resources:{}};async function a(){var a;await(a="https://unpkg.com/i18next@latest/i18next.min.js",new Promise((t,e)=>{if(window.i18next)return t();const n=document.createElement("script");n.src=a,n.onload=t,n.onerror=()=>e(new Error(`Failed to load ${a}`)),document.head.appendChild(n)}));let n="en";State.variables[t]?n=State.variables[t]:navigator.language&&(n=navigator.language.split("-")[0]),await i18next.init({...e,lng:n,interpolation:{escapeValue:!1}}),State.variables[t]||(State.variables[t]=i18next.language),console.log(`[i18n] Initialized (${i18next.language})`)}function n(t,e={}){return window.i18next?i18next.t(t,e):t}Macro.add("loadTranslations",{handler:function(){if(this.args.length<2)return this.error("Usage: <<loadTranslations 'path' 'langCode'>>");const t=this.args[0],e=this.args[1];fetch(t).then(t=>{if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);return t.json()}).then(a=>{window.i18next&&(i18next.addResourceBundle(e,"translation",a,!0,!0),console.log(`[i18n] Loaded translations for ${e} from ${t}`),i18next.language===e&&Engine.show())}).catch(e=>{console.error(`[i18n] Failed to load ${t}:`,e),$(this.output).append(`<span class="error">i18n Error: Failed to load ${t}</span>`)})}}),Macro.add("setLang",{handler(){(function(e){return i18next.changeLanguage(e).then(()=>{State.variables[t]=e})})(this.args[0]).then(()=>Engine.show())}}),Macro.add("t",{handler(){if(0===this.args.length)return this.error("Usage: <<t 'key' [options]>>");const t=this.args[0];let e={};const a=this.args[1];if(this.args.length>1&&"object"==typeof a&&!Array.isArray(a))e=a;else{if((this.args.length-1)%2!=0)return this.error("<<t>> expects key-value pairs or an options object");for(let t=1;t<this.args.length;t+=2)e[this.args[t]]=this.args[t+1]}const r=n(t,e);$(this.output).append($("<span>").html(r))}}),Macro.add("tlink",{handler(){const t=this.args[0],e=this.args[1];let a={};if(this.args.length>2){const t=this.args[2];if(t&&"object"==typeof t)a=t;else for(let t=2;t<this.args.length;t+=2)a[this.args[t]]=this.args[t+1]}const r=n(t,a);new Wikifier(this.output,`<<link "${r}" "${e}">><</link>>`)}}),async function(){try{var t=LoadScreen.lock();await a(),LoadScreen.unlock(t)}catch(t){console.error("[i18n] Init failed",t)}}(),Save.onLoad.add(e=>{const a=e?.state?.history?.[e.state.index],n=a?.variables?.[t];n&&window.i18next&&i18next.changeLanguage(n)}),$(document).on(":storyready",()=>{window.i18next&&State.variables[t]&&i18next.changeLanguage(State.variables[t])});
2
+ //# sourceMappingURL=sugarcube-i18n.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sugarcube-i18n.esm.js","sources":["../src/core.js","../src/macros.js","../src/index.js"],"sourcesContent":["\"use strict\";\r\n\r\nexport const STATE_VAR_NAME = \"lang\";\r\nconst I18NEXT_CDN = \"https://unpkg.com/i18next@latest/i18next.min.js\";\r\n\r\nconst i18nOptions = {\r\n debug: true,\r\n fallbackLng: \"en\",\r\n resources: {}\r\n};\r\n\r\nfunction loadScript(src) {\r\n // Load the script\r\n return new Promise((resolve, reject) => {\r\n // If i18next is already loaded, return it\r\n if (window.i18next) return resolve(); \r\n const script = document.createElement(\"script\");\r\n script.src = src;\r\n script.onload = resolve;\r\n script.onerror = () => reject(new Error(`Failed to load ${src}`));\r\n document.head.appendChild(script);\r\n });\r\n}\r\n\r\nexport async function initI18n() {\r\n await loadScript(I18NEXT_CDN);\r\n\r\n let initialLang = \"en\";\r\n if (State.variables[STATE_VAR_NAME]) {\r\n initialLang = State.variables[STATE_VAR_NAME];\r\n } else if (navigator.language) {\r\n initialLang = navigator.language.split(\"-\")[0];\r\n }\r\n\r\n await i18next.init({\r\n ...i18nOptions,\r\n lng: initialLang,\r\n interpolation: { escapeValue: false }\r\n });\r\n\r\n if (!State.variables[STATE_VAR_NAME]) {\r\n State.variables[STATE_VAR_NAME] = i18next.language;\r\n }\r\n\r\n console.log(`[i18n] Initialized (${i18next.language})`);\r\n}\r\n\r\nexport function setLang(lang) {\r\n return i18next.changeLanguage(lang).then(() => {\r\n State.variables[STATE_VAR_NAME] = lang;\r\n });\r\n}\r\n\r\nexport function translate(key, options = {}) {\r\n return window.i18next ? i18next.t(key, options) : key;\r\n}\r\n","import { translate, setLang } from \"./core.js\";\r\n\r\nexport function registerMacros() {\r\n\r\n /**\r\n * Macro: <<loadTranslations \"path/to/file.json\" [namespace]>>\r\n * Loads a JSON file and adds it to i18next resources.\r\n *\r\n * Convention:\r\n * The JSON file should be structured as:\r\n * {\r\n * \"key\": \"translation\"\r\n * }\r\n *\r\n * Usage: <<loadTranslations \"locales/en.json\" \"en\">>\r\n */\r\n Macro.add('loadTranslations', {\r\n handler: function () {\r\n if (this.args.length < 2) {\r\n return this.error(\"Usage: <<loadTranslations 'path' 'langCode'>>\");\r\n }\r\n\r\n const path = this.args[0];\r\n const lang = this.args[1];\r\n const ns = 'translation'; // default namespace\r\n\r\n // fetch is async, but macros in Init/StoryInit are synchronous. \r\n // SugarCube doesn't pause for async macros in Init usually, but content won't render till ready if used in Passages.\r\n \r\n fetch(path)\r\n .then(response => {\r\n if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);\r\n return response.json();\r\n })\r\n .then(data => {\r\n if (window.i18next) {\r\n i18next.addResourceBundle(lang, ns, data, true, true);\r\n console.log(`[i18n] Loaded translations for ${lang} from ${path}`);\r\n // If this is the current language, trigger a refresh might be needed if passage is already shown?\r\n // Usually this is done in StoryInit so no passage is shown yet.\r\n \r\n // Force update engine if we are currently looking at this language\r\n if (i18next.language === lang) {\r\n Engine.show(); // Refreshes current passage if needed, though be careful in Init\r\n }\r\n }\r\n })\r\n .catch(err => {\r\n console.error(`[i18n] Failed to load ${path}:`, err);\r\n $(this.output).append(`<span class=\"error\">i18n Error: Failed to load ${path}</span>`);\r\n });\r\n }\r\n });\r\n\r\n /**\r\n * Macro: <<setLang \"code\">>\r\n * Changes the current language and updates persistence.\r\n */\r\n Macro.add('setLang', {\r\n handler() { \r\n const lang = this.args[0]; \r\n setLang(lang).then(() => Engine.show()); \r\n } \r\n });\r\n\r\n /**\r\n * Macro: <<t \"key\" [options]>>\r\n * Translates a key. Supports interpolation.\r\n * Options can be an object or key-value pairs.\r\n * Example:\r\n * - Object options: \r\n * <<set $opts to { name: $name }>>\r\n * <<t \"greeting\" $opts>>\r\n * - Key-Value pairs options:\r\n * <<t \"greeting\" \"name\" $name>>\r\n */\r\n Macro.add('t', {\r\n handler() {\r\n if (this.args.length === 0) {\r\n return this.error(\"Usage: <<t 'key' [options]>>\");\r\n }\r\n\r\n const key = this.args[0];\r\n let options = {};\r\n const second = this.args[1];\r\n if (this.args.length > 1 && typeof second === \"object\" && !Array.isArray(second)) {\r\n // Object form\r\n options = second;\r\n } else {\r\n // Key-value pairs form\r\n if ((this.args.length - 1) % 2 !== 0) {\r\n return this.error(\r\n \"<<t>> expects key-value pairs or an options object\"\r\n );\r\n }\r\n\r\n for (let i = 1; i < this.args.length; i += 2) {\r\n options[this.args[i]] = this.args[i + 1];\r\n }\r\n }\r\n\r\n const text = translate(key, options);\r\n $(this.output).append($(\"<span>\").html(text));\r\n }\r\n });\r\n\r\n /**\r\n * Macro: <<tlink \"key\" \"passage\" [options]>>\r\n * Translates a key and creates a link to a passage. Supports interpolation.\r\n * Options can be an object or key-value pairs.\r\n * Example:\r\n * <<tlink \"go-to-forest\" \"ForestPassage\">>\r\n * <<tlink \"go-to-forest\" \"ForestPassage\" { name: $name }>>\r\n * <<tlink \"go-to-forest\" \"ForestPassage\" \"name\" $name>>\r\n */\r\n Macro.add(\"tlink\", {\r\n handler() {\r\n const key = this.args[0];\r\n const passage = this.args[1];\r\n let options = {}; \r\n\r\n if (this.args.length > 2) {\r\n const second = this.args[2];\r\n if (second && typeof second === \"object\") {\r\n options = second;\r\n } else {\r\n for (let i = 2; i < this.args.length; i += 2) {\r\n options[this.args[i]] = this.args[i + 1];\r\n }\r\n }\r\n }\r\n const label = translate(key, options);\r\n new Wikifier(this.output, `<<link \"${label}\" \"${passage}\">><</link>>`);\r\n }\r\n });\r\n\r\n}\r\n","import { initI18n, STATE_VAR_NAME } from \"./core.js\";\r\nimport { registerMacros } from \"./macros.js\";\r\n\r\nregisterMacros();\r\n\r\n(async function () {\r\n try {\r\n\r\n // Lock the loading screen.\r\n var lockId = LoadScreen.lock();\r\n\r\n // Initialize i18n plugin configuration\r\n await initI18n();\r\n \r\n // Release the loading screen.\r\n LoadScreen.unlock(lockId);\r\n \r\n } catch (err) {\r\n console.error(\"[i18n] Init failed\", err);\r\n }\r\n})();\r\n\r\n// Save / Load hooks\r\nSave.onLoad.add(save => {\r\n const moment = save?.state?.history?.[save.state.index];\r\n const lang = moment?.variables?.[STATE_VAR_NAME];\r\n if (lang && window.i18next) {\r\n i18next.changeLanguage(lang);\r\n }\r\n});\r\n\r\n$(document).on(\":storyready\", () => {\r\n if (window.i18next && State.variables[STATE_VAR_NAME]) {\r\n i18next.changeLanguage(State.variables[STATE_VAR_NAME]);\r\n }\r\n});\r\n"],"names":["STATE_VAR_NAME","i18nOptions","debug","fallbackLng","resources","async","initI18n","src","Promise","resolve","reject","window","i18next","script","document","createElement","onload","onerror","Error","head","appendChild","initialLang","State","variables","navigator","language","split","init","lng","interpolation","escapeValue","console","log","translate","key","options","t","Macro","add","handler","this","args","length","error","path","lang","fetch","then","response","ok","status","json","data","addResourceBundle","Engine","show","catch","err","$","output","append","changeLanguage","setLang","second","Array","isArray","i","text","html","passage","label","Wikifier","lockId","LoadScreen","lock","unlock","Save","onLoad","save","moment","state","history","index","on"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ !function(){"use strict";const t="lang",e={debug:!0,fallbackLng:"en",resources:{}};async function a(){var a;await(a="https://unpkg.com/i18next@latest/i18next.min.js",new Promise((t,e)=>{if(window.i18next)return t();const n=document.createElement("script");n.src=a,n.onload=t,n.onerror=()=>e(new Error(`Failed to load ${a}`)),document.head.appendChild(n)}));let n="en";State.variables[t]?n=State.variables[t]:navigator.language&&(n=navigator.language.split("-")[0]),await i18next.init({...e,lng:n,interpolation:{escapeValue:!1}}),State.variables[t]||(State.variables[t]=i18next.language),console.log(`[i18n] Initialized (${i18next.language})`)}function n(t,e={}){return window.i18next?i18next.t(t,e):t}Macro.add("loadTranslations",{handler:function(){if(this.args.length<2)return this.error("Usage: <<loadTranslations 'path' 'langCode'>>");const t=this.args[0],e=this.args[1];fetch(t).then(t=>{if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);return t.json()}).then(a=>{window.i18next&&(i18next.addResourceBundle(e,"translation",a,!0,!0),console.log(`[i18n] Loaded translations for ${e} from ${t}`),i18next.language===e&&Engine.show())}).catch(e=>{console.error(`[i18n] Failed to load ${t}:`,e),$(this.output).append(`<span class="error">i18n Error: Failed to load ${t}</span>`)})}}),Macro.add("setLang",{handler(){(function(e){return i18next.changeLanguage(e).then(()=>{State.variables[t]=e})})(this.args[0]).then(()=>Engine.show())}}),Macro.add("t",{handler(){if(0===this.args.length)return this.error("Usage: <<t 'key' [options]>>");const t=this.args[0];let e={};const a=this.args[1];if(this.args.length>1&&"object"==typeof a&&!Array.isArray(a))e=a;else{if((this.args.length-1)%2!=0)return this.error("<<t>> expects key-value pairs or an options object");for(let t=1;t<this.args.length;t+=2)e[this.args[t]]=this.args[t+1]}const r=n(t,e);$(this.output).append($("<span>").html(r))}}),Macro.add("tlink",{handler(){const t=this.args[0],e=this.args[1];let a={};if(this.args.length>2){const t=this.args[2];if(t&&"object"==typeof t)a=t;else for(let t=2;t<this.args.length;t+=2)a[this.args[t]]=this.args[t+1]}const r=n(t,a);new Wikifier(this.output,`<<link "${r}" "${e}">><</link>>`)}}),async function(){try{var t=LoadScreen.lock();await a(),LoadScreen.unlock(t)}catch(t){console.error("[i18n] Init failed",t)}}(),Save.onLoad.add(e=>{const a=e?.state?.history?.[e.state.index],n=a?.variables?.[t];n&&window.i18next&&i18next.changeLanguage(n)}),$(document).on(":storyready",()=>{window.i18next&&State.variables[t]&&i18next.changeLanguage(State.variables[t])})}();
2
+ //# sourceMappingURL=sugarcube-i18n.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sugarcube-i18n.js","sources":["../src/core.js","../src/macros.js","../src/index.js"],"sourcesContent":["\"use strict\";\r\n\r\nexport const STATE_VAR_NAME = \"lang\";\r\nconst I18NEXT_CDN = \"https://unpkg.com/i18next@latest/i18next.min.js\";\r\n\r\nconst i18nOptions = {\r\n debug: true,\r\n fallbackLng: \"en\",\r\n resources: {}\r\n};\r\n\r\nfunction loadScript(src) {\r\n // Load the script\r\n return new Promise((resolve, reject) => {\r\n // If i18next is already loaded, return it\r\n if (window.i18next) return resolve(); \r\n const script = document.createElement(\"script\");\r\n script.src = src;\r\n script.onload = resolve;\r\n script.onerror = () => reject(new Error(`Failed to load ${src}`));\r\n document.head.appendChild(script);\r\n });\r\n}\r\n\r\nexport async function initI18n() {\r\n await loadScript(I18NEXT_CDN);\r\n\r\n let initialLang = \"en\";\r\n if (State.variables[STATE_VAR_NAME]) {\r\n initialLang = State.variables[STATE_VAR_NAME];\r\n } else if (navigator.language) {\r\n initialLang = navigator.language.split(\"-\")[0];\r\n }\r\n\r\n await i18next.init({\r\n ...i18nOptions,\r\n lng: initialLang,\r\n interpolation: { escapeValue: false }\r\n });\r\n\r\n if (!State.variables[STATE_VAR_NAME]) {\r\n State.variables[STATE_VAR_NAME] = i18next.language;\r\n }\r\n\r\n console.log(`[i18n] Initialized (${i18next.language})`);\r\n}\r\n\r\nexport function setLang(lang) {\r\n return i18next.changeLanguage(lang).then(() => {\r\n State.variables[STATE_VAR_NAME] = lang;\r\n });\r\n}\r\n\r\nexport function translate(key, options = {}) {\r\n return window.i18next ? i18next.t(key, options) : key;\r\n}\r\n","import { translate, setLang } from \"./core.js\";\r\n\r\nexport function registerMacros() {\r\n\r\n /**\r\n * Macro: <<loadTranslations \"path/to/file.json\" [namespace]>>\r\n * Loads a JSON file and adds it to i18next resources.\r\n *\r\n * Convention:\r\n * The JSON file should be structured as:\r\n * {\r\n * \"key\": \"translation\"\r\n * }\r\n *\r\n * Usage: <<loadTranslations \"locales/en.json\" \"en\">>\r\n */\r\n Macro.add('loadTranslations', {\r\n handler: function () {\r\n if (this.args.length < 2) {\r\n return this.error(\"Usage: <<loadTranslations 'path' 'langCode'>>\");\r\n }\r\n\r\n const path = this.args[0];\r\n const lang = this.args[1];\r\n const ns = 'translation'; // default namespace\r\n\r\n // fetch is async, but macros in Init/StoryInit are synchronous. \r\n // SugarCube doesn't pause for async macros in Init usually, but content won't render till ready if used in Passages.\r\n \r\n fetch(path)\r\n .then(response => {\r\n if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);\r\n return response.json();\r\n })\r\n .then(data => {\r\n if (window.i18next) {\r\n i18next.addResourceBundle(lang, ns, data, true, true);\r\n console.log(`[i18n] Loaded translations for ${lang} from ${path}`);\r\n // If this is the current language, trigger a refresh might be needed if passage is already shown?\r\n // Usually this is done in StoryInit so no passage is shown yet.\r\n \r\n // Force update engine if we are currently looking at this language\r\n if (i18next.language === lang) {\r\n Engine.show(); // Refreshes current passage if needed, though be careful in Init\r\n }\r\n }\r\n })\r\n .catch(err => {\r\n console.error(`[i18n] Failed to load ${path}:`, err);\r\n $(this.output).append(`<span class=\"error\">i18n Error: Failed to load ${path}</span>`);\r\n });\r\n }\r\n });\r\n\r\n /**\r\n * Macro: <<setLang \"code\">>\r\n * Changes the current language and updates persistence.\r\n */\r\n Macro.add('setLang', {\r\n handler() { \r\n const lang = this.args[0]; \r\n setLang(lang).then(() => Engine.show()); \r\n } \r\n });\r\n\r\n /**\r\n * Macro: <<t \"key\" [options]>>\r\n * Translates a key. Supports interpolation.\r\n * Options can be an object or key-value pairs.\r\n * Example:\r\n * - Object options: \r\n * <<set $opts to { name: $name }>>\r\n * <<t \"greeting\" $opts>>\r\n * - Key-Value pairs options:\r\n * <<t \"greeting\" \"name\" $name>>\r\n */\r\n Macro.add('t', {\r\n handler() {\r\n if (this.args.length === 0) {\r\n return this.error(\"Usage: <<t 'key' [options]>>\");\r\n }\r\n\r\n const key = this.args[0];\r\n let options = {};\r\n const second = this.args[1];\r\n if (this.args.length > 1 && typeof second === \"object\" && !Array.isArray(second)) {\r\n // Object form\r\n options = second;\r\n } else {\r\n // Key-value pairs form\r\n if ((this.args.length - 1) % 2 !== 0) {\r\n return this.error(\r\n \"<<t>> expects key-value pairs or an options object\"\r\n );\r\n }\r\n\r\n for (let i = 1; i < this.args.length; i += 2) {\r\n options[this.args[i]] = this.args[i + 1];\r\n }\r\n }\r\n\r\n const text = translate(key, options);\r\n $(this.output).append($(\"<span>\").html(text));\r\n }\r\n });\r\n\r\n /**\r\n * Macro: <<tlink \"key\" \"passage\" [options]>>\r\n * Translates a key and creates a link to a passage. Supports interpolation.\r\n * Options can be an object or key-value pairs.\r\n * Example:\r\n * <<tlink \"go-to-forest\" \"ForestPassage\">>\r\n * <<tlink \"go-to-forest\" \"ForestPassage\" { name: $name }>>\r\n * <<tlink \"go-to-forest\" \"ForestPassage\" \"name\" $name>>\r\n */\r\n Macro.add(\"tlink\", {\r\n handler() {\r\n const key = this.args[0];\r\n const passage = this.args[1];\r\n let options = {}; \r\n\r\n if (this.args.length > 2) {\r\n const second = this.args[2];\r\n if (second && typeof second === \"object\") {\r\n options = second;\r\n } else {\r\n for (let i = 2; i < this.args.length; i += 2) {\r\n options[this.args[i]] = this.args[i + 1];\r\n }\r\n }\r\n }\r\n const label = translate(key, options);\r\n new Wikifier(this.output, `<<link \"${label}\" \"${passage}\">><</link>>`);\r\n }\r\n });\r\n\r\n}\r\n","import { initI18n, STATE_VAR_NAME } from \"./core.js\";\r\nimport { registerMacros } from \"./macros.js\";\r\n\r\nregisterMacros();\r\n\r\n(async function () {\r\n try {\r\n\r\n // Lock the loading screen.\r\n var lockId = LoadScreen.lock();\r\n\r\n // Initialize i18n plugin configuration\r\n await initI18n();\r\n \r\n // Release the loading screen.\r\n LoadScreen.unlock(lockId);\r\n \r\n } catch (err) {\r\n console.error(\"[i18n] Init failed\", err);\r\n }\r\n})();\r\n\r\n// Save / Load hooks\r\nSave.onLoad.add(save => {\r\n const moment = save?.state?.history?.[save.state.index];\r\n const lang = moment?.variables?.[STATE_VAR_NAME];\r\n if (lang && window.i18next) {\r\n i18next.changeLanguage(lang);\r\n }\r\n});\r\n\r\n$(document).on(\":storyready\", () => {\r\n if (window.i18next && State.variables[STATE_VAR_NAME]) {\r\n i18next.changeLanguage(State.variables[STATE_VAR_NAME]);\r\n }\r\n});\r\n"],"names":["STATE_VAR_NAME","i18nOptions","debug","fallbackLng","resources","async","initI18n","src","Promise","resolve","reject","window","i18next","script","document","createElement","onload","onerror","Error","head","appendChild","initialLang","State","variables","navigator","language","split","init","lng","interpolation","escapeValue","console","log","translate","key","options","t","Macro","add","handler","this","args","length","error","path","lang","fetch","then","response","ok","status","json","data","addResourceBundle","Engine","show","catch","err","$","output","append","changeLanguage","setLang","second","Array","isArray","i","text","html","passage","label","Wikifier","lockId","LoadScreen","lock","unlock","Save","onLoad","save","moment","state","history","index","on"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ sugarcube-2:
2
+ macros:
3
+ loadTranslations:
4
+ name: loadTranslations
5
+ description: "Loads a JSON translation file."
6
+ parameters:
7
+ - "path"
8
+ - "language"
9
+ setLang:
10
+ name: setLang
11
+ description: "Sets the current language."
12
+ parameters:
13
+ - "language_code"
14
+ t:
15
+ name: t
16
+ description: "Translates a key."
17
+ parameters:
18
+ - "key"
19
+ - "options"
20
+ tlink:
21
+ name: tlink
22
+ description: "Translates a key and creates a link to a passage."
23
+ parameters:
24
+ - "key"
25
+ - "passage"
26
+ - "options"
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "sugarcube-i18n",
3
+ "version": "0.1.0",
4
+ "description": "i18n plugin for Twine SugarCube",
5
+ "license": "MIT",
6
+ "author": "@GazzD",
7
+ "homepage": "https://github.com/GazzD/sugarcube-i18n",
8
+ "type": "module",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/GazzD/sugarcube-i18n.git"
12
+ },
13
+ "keywords": [
14
+ "twine",
15
+ "sugarcube",
16
+ "i18n",
17
+ "i18next",
18
+ "interactive-fiction",
19
+ "narrative",
20
+ "plugin"
21
+ ],
22
+ "main": "dist/sugarcube-i18n.js",
23
+ "module": "dist/sugarcube-i18n.esm.js",
24
+ "files": [
25
+ "dist",
26
+ "editor",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "scripts": {
31
+ "build": "rollup -c",
32
+ "dev": "rollup -c -w",
33
+ "clean": "rimraf dist",
34
+ "prepublishOnly": "npm run clean && npm run build"
35
+ },
36
+ "peerDependencies": {
37
+ "i18next": ">=22"
38
+ },
39
+ "devDependencies": {
40
+ "@rollup/plugin-commonjs": "^25.0.8",
41
+ "@rollup/plugin-node-resolve": "^15.3.1",
42
+ "@rollup/plugin-terser": "^0.4.4",
43
+ "rimraf": "^5.0.0",
44
+ "rollup": "^4.54.0"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/GazzD/sugarcube-i18n/issues"
48
+ }
49
+ }