-
-
Notifications
You must be signed in to change notification settings - Fork 36
feat: paraglide
adder
#67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
66 commits
Select commit
Hold shift + click to select a range
0c2d3de
start building the Paraglide adder
LorisSigrist 556d242
Add a bunch of config
LorisSigrist 0f070b4
Add the handle hook
LorisSigrist 427eeef
Validate input & custom placeholder
LorisSigrist 8c6263d
Create example message file
LorisSigrist 5d4c6c6
Automatically add `sequence` if handle hook is already defined
LorisSigrist 75cfd9b
comments
LorisSigrist 4fb5bf1
Fix typo in inlang config
LorisSigrist 8392626
Update the app.html file
LorisSigrist 22a2d4a
Revert accidental gitignore change
LorisSigrist 475cc98
Fix types
LorisSigrist ebb7148
Add link to original Regex
LorisSigrist 344a6c8
normalize comment formatting
LorisSigrist cfd399a
don't change gitignore
LorisSigrist 234ceda
print invalid language tags
LorisSigrist 05c6d8b
Pin version numbers
LorisSigrist ba7e080
Add changeset
LorisSigrist 0943224
move adder
manuel3108 73f952a
migrate
manuel3108 7a8d405
remove implemented todo
manuel3108 9ebdd8c
fix adder
manuel3108 9e1b0be
make it work
manuel3108 76cd423
Update packages/adders/paraglide/config/adder.ts
manuel3108 a17fbdd
Merge branch 'main' into feat/paraglide
manuel3108 ae8b8c7
fix type errors
manuel3108 5e2a466
robustness
manuel3108 49c7526
fix warnings
manuel3108 517e52a
fix new lines
manuel3108 3f1adda
remove old changeset
manuel3108 e0fe069
use log.warn
manuel3108 7e93368
add comment
manuel3108 986ea1f
fix lint
manuel3108 de742f8
coerce to a number
AdrianGonz97 e7f8701
Update packages/adders/paraglide/config/adder.ts
manuel3108 f5d005a
Update packages/adders/paraglide/config/adder.ts
manuel3108 2fa31a1
Update packages/adders/paraglide/config/adder.ts
manuel3108 1b5d560
Update packages/adders/paraglide/config/adder.ts
manuel3108 ee1997f
add jsdoc utility method
manuel3108 ebef502
remove useless imports
manuel3108 d6c86a7
only add jsdoc comment if not in typescript context
manuel3108 2a32926
Merge branch 'main' into feat/paraglide
Manuel-Innovapps 57f396c
update to new syntax
manuel3108 a5b35fc
update adder
manuel3108 e5e138d
disable `noUncheckedIndexedAccess`
manuel3108 3d13a9d
Merge branch 'main' into feat/paraglide
manuel3108 3b1405d
Merge branch 'main' into feat/paraglide
manuel3108 603ee58
update to new syntax
manuel3108 d2d13fb
fix
manuel3108 0efd31a
Merge branch 'main' into feat/paraglide
manuel3108 0e39a5c
use new slot feature
manuel3108 252c9ee
newline
benmccann b0f7bbf
Merge branch 'main' into feat/paraglide
benmccann da38ed0
Merge branch 'main' into feat/paraglide
benmccann da11218
update imports
AdrianGonz97 6ed51b8
upgrade magic-string. doesn't help, but might as well be on latest
benmccann 2b7c924
should generally use update instead of overwrite. though it doesn't help
benmccann 1759612
fix
benmccann bf37e8d
changeset
manuel3108 0adb8d1
improve formatting
manuel3108 fb37e15
improve output
manuel3108 62e8d15
fix command line parsing
manuel3108 642e095
Merge branch 'main' into feat/paraglide
manuel31
8000
08 c811797
Revert "improve output"
manuel3108 9f5f09e
fix merge conflicts
manuel3108 ef55cb6
remove useless function
manuel3108 63e065a
fix
manuel3108 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'sv': patch | ||
--- | ||
|
||
feat: paraglide adder |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode cha
8000
racters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,354 @@ | ||
import fs from 'node:fs'; | ||
import path from 'node:path'; | ||
import { defineAdder, defineAdderOptions, log, utils } from '@sveltejs/cli-core'; | ||
import { | ||
array, | ||
common, | ||
functions, | ||
imports, | ||
object, | ||
variables, | ||
exports, | ||
kit | ||
} from '@sveltejs/cli-core/js'; | ||
import * as html from '@sveltejs/cli-core/html'; | ||
import { parseHtml, parseJson, parseScript, parseSvelte } from '@sveltejs/cli-core/parsers'; | ||
|
||
const DEFAULT_INLANG_PROJECT = { | ||
$schema: 'https://inlang.com/schema/project-settings', | ||
modules: [ | ||
'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@1/dist/index.js', | ||
'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-identical-pattern@1/dist/index.js', | ||
'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@1/dist/index.js', | ||
'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@1/dist/index.js', | ||
'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@1/dist/index.js', | ||
'https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@2/dist/index.js', | ||
'https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@0/dist/index.js' | ||
], | ||
'plugin.inlang.messageFormat': { | ||
pathPattern: './messages/{languageTag}.json' | ||
} | ||
}; | ||
|
||
export const options = defineAdderOptions({ | ||
availableLanguageTags: { | ||
question: 'Which language tags would you like to support?', | ||
type: 'string', | ||
default: '', | ||
placeholder: 'en,de-ch', | ||
validate(input: any) { | ||
const { invalidLanguageTags, validLanguageTags } = parseLanguageTagInput(input); | ||
|
||
if (invalidLanguageTags.length > 0) { | ||
if (invalidLanguageTags.length === 1) { | ||
return `The input "${invalidLanguageTags[0]}" is not a valid BCP47 language tag`; | ||
} else { | ||
const listFormat = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }); | ||
return `The inputs ${listFormat.format(invalidLanguageTags.map((x) => `"${x}"`))} are not valid BCP47 language tags`; | ||
} | ||
} | ||
if (validLanguageTags.length === 0) | ||
return 'Please enter at least one valid BCP47 language tag. Eg: en'; | ||
|
||
return undefined; | ||
} | ||
}, | ||
demo: { | ||
type: 'boolean', | ||
default: false, | ||
question: 'Do you want to include a demo?' | ||
} | ||
}); | ||
|
||
export default defineAdder({ | ||
id: 'paraglide', | ||
name: 'Paraglide', | ||
description: 'Typesafe i18n with localised routing', | ||
environments: { svelte: false, kit: true }, | ||
documentation: 'https://inlang.com/m/dxnzrydw/paraglide-sveltekit-i18n', | ||
options, | ||
packages: [ | ||
{ | ||
name: '@inlang/paraglide-sveltekit', | ||
version: '^0.11.1', | ||
dev: false | ||
} | ||
], | ||
files: [ | ||
{ | ||
// create an inlang project if it doesn't exist yet | ||
name: () => 'project.inlang/settings.json', | ||
condition: ({ cwd }) => !fs.existsSync(path.join(cwd, 'project.inlang/settings.json')), | ||
content: ({ options, content }) => { | ||
const { data, generateCode } = parseJson(content); | ||
|
||
for (const key in DEFAULT_INLANG_PROJECT) { | ||
data[key] = DEFAULT_INLANG_PROJECT[key as keyof typeof DEFAULT_INLANG_PROJECT]; | ||
} | ||
const { validLanguageTags } = parseLanguageTagInput(options.availableLanguageTags); | ||
const sourceLanguageTag = validLanguageTags[0]; | ||
|
||
data.sourceLanguageTag = sourceLanguageTag; | ||
data.languageTags = validLanguageTags; | ||
|
||
return generateCode(); | ||
} | ||
}, | ||
{ | ||
// add the vite plugin | ||
name: ({ typescript }) => `vite.config.${typescript ? 'ts' : 'js'}`, | ||
content: ({ content }) => { | ||
const { ast, generateCode } = parseScript(content); | ||
|
||
const vitePluginName = 'paraglide'; | ||
imports.addNamed(ast, '@inlang/paraglide-sveltekit/vite', { | ||
paraglide: vitePluginName | ||
}); | ||
|
||
const { value: rootObject } = exports.defaultExport( | ||
ast, | ||
functions.call('defineConfig', []) | ||
); | ||
const param1 = functions.argumentByIndex(rootObject, 0, object.createEmpty()); | ||
|
||
const pluginsArray = object.property(param1, 'plugins', array.createEmpty()); | ||
const pluginFunctionCall = functions.call(vitePluginName, []); | ||
const pluginConfig = object.create({ | ||
project: common.createLiteral('./project.inlang'), | ||
outdir: common.createLiteral('./src/lib/paraglide') | ||
manuel3108 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
functions.argumentByIndex(pluginFunctionCall, 0, pluginConfig); | ||
array.push(pluginsArray, pluginFunctionCall); | ||
|
||
return generateCode(); | ||
} | ||
}, | ||
{ | ||
// src/lib/i18n file | ||
name: ({ typescript }) => `src/lib/i18n.${typescript ? 'ts' : 'js'}`, | ||
content({ content }) { | ||
const { ast, generateCode } = parseScript(content); | ||
|
||
imports.addNamed(ast, '@inlang/paraglide-sveltekit', { createI18n: 'createI18n' }); | ||
imports.addDefault(ast, '$lib/paraglide/runtime', '* as runtime'); | ||
|
||
const createI18nExpression = common.expressionFromString('createI18n(runtime)'); | ||
const i18n = variables.declaration(ast, 'const', 'i18n', createI18nExpression); | ||
|
||
const existingExport = exports.namedExport(ast, 'i18n', i18n); | ||
if (existingExport.declaration != i18n) { | ||
log.warn('Setting up $lib/i18n failed because it already exports an i18n function'); | ||
} | ||
|
||
return generateCode(); | ||
} | ||
}, | ||
{ | ||
// reroute hook | ||
name: ({ typescript }) => `src/hooks.${typescript ? 'ts' : 'js'}`, | ||
content({ content }) { | ||
const { ast, generateCode } = parseScript(content); | ||
|
||
imports.addNamed(ast, '$lib/i18n', { | ||
i18n: 'i18n' | ||
}); | ||
|
||
const expression = common.expressionFromString('i18n.reroute()'); | ||
const rerouteIdentifier = variables.declaration(ast, 'const', 'reroute', expression); | ||
|
||
const existingExport = exports.namedExport(ast, 'reroute', rerouteIdentifier); | ||
if (existingExport.declaration != rerouteIdentifier) { | ||
log.warn('Adding the reroute hook automatically failed. Add it manually'); | ||
} | ||
|
||
return generateCode(); | ||
} | ||
}, | ||
{ | ||
// handle hook | ||
name: ({ typescript }) => `src/hooks.server.${typescript ? 'ts' : 'js'}`, | ||
content({ content, typescript }) { | ||
const { ast, generateCode } = parseScript(content); | ||
F438 |
|
|
imports.addNamed(ast, '$lib/i18n', { | ||
i18n: 'i18n' | ||
}); | ||
|
||
const hookHandleContent = 'i18n.handle()'; | ||
kit.addHooksHandle(ast, typescript, 'paraglide', hookHandleContent); | ||
|
||
return generateCode(); | ||
} | ||
}, | ||
{ | ||
// add the <ParaglideJS> component to the layout | ||
name: ({ kit }) => `${kit?.routesDirectory}/+layout.svelte`, | ||
content: ({ content, dependencyVersion }) => { | ||
const { script, template, generateCode } = parseSvelte(content); | ||
|
||
const paraglideComponentName = 'ParaglideJS'; | ||
manuel3108 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
imports.addNamed(script.ast, '@inlang/paraglide-sveltekit', { | ||
[paraglideComponentName]: paraglideComponentName | ||
}); | ||
imports.addNamed(script.ast, '$lib/i18n', { | ||
i18n: 'i18n' | ||
}); | ||
|
||
// wrap the HTML in a ParaglideJS instance | ||
const rootChildren = template.ast.children; | ||
if (rootChildren.length === 0) { | ||
const svelteVersion = dependencyVersion('svelte'); | ||
if (!svelteVersion) throw new Error('Failed to determine svelte version'); | ||
|
||
html.addSlot(script.ast, template.ast, svelteVersion); | ||
} | ||
|
||
const hasParaglideJsNode = rootChildren.find( | ||
(x) => x.type == 'tag' && x.name == paraglideComponentName | ||
); | ||
if (!hasParaglideJsNode) { | ||
const root = html.element(paraglideComponentName, {}); | ||
root.attribs = { | ||
'{i18n}': '' | ||
}; | ||
root.children = rootChildren; | ||
template.ast.children = [root]; | ||
} | ||
|
||
return generateCode({ script: script.generateCode(), template: template.generateCode() }); | ||
} | ||
}, | ||
{ | ||
// add the text-direction and lang attribute placeholders to app.html | ||
name: () => 'src/app.html', | ||
content: ({ content }) => { | ||
const { ast, generateCode } = parseHtml(content); | ||
|
||
const htmlNode = ast.children.find( | ||
(child): child is html.HtmlElement => | ||
child.type === html.HtmlElementType.Tag && child.name === 'html' | ||
); | ||
if (!htmlNode) { | ||
log.warn( | ||
"Could not find <html> node in app.html. You'll need to add the language placeholder manually" | ||
); | ||
return generateCode(); | ||
} | ||
htmlNode.attribs = { | ||
...htmlNode.attribs, | ||
lang: '%paraglide.lang%', | ||
dir: '%paraglide.textDirection%' | ||
}; | ||
|
||
return generateCode(); | ||
} | ||
}, | ||
{ | ||
// add usage example | ||
name: ({ kit }) => `${kit?.routesDirectory}/+page.svelte`, | ||
condition: ({ options }) => options.demo, | ||
content({ content, options, typescript }) { | ||
const { script, template, generateCode } = parseSvelte(content); | ||
|
||
imports.addDefault(script.ast, '$lib/paraglide/messages.js', '* as m'); | ||
imports.addNamed(script.ast, '$app/navigation', { goto: 'goto' }); | ||
imports.addNamed(script.ast, '$app/stores', { page: 'page' }); | ||
imports.addNamed(script.ast, '$lib/i18n', { i18n: 'i18n' }); | ||
if (typescript) { | ||
imports.addNamed( | ||
script.ast, | ||
'$lib/paraglide/runtime', | ||
{ AvailableLanguageTag: 'AvailableLanguageTag' }, | ||
true | ||
); | ||
} | ||
|
||
const { ts } = utils.createPrinter({ ts: typescript }); | ||
manuel3108 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const methodStatement = common.statementFromString(` | ||
function switchToLanguage(newLanguage${ts(': AvailableLanguageTag')}) { | ||
const canonicalPath = i18n.route($page.url.pathname); | ||
const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage); | ||
goto(localisedPath); | ||
} | ||
`); | ||
if (!typescript) { | ||
common.addJsDocComment(methodStatement, { | ||
'import("$lib/paraglide/runtime").AvailableLanguageTag': 'newLanguage' | ||
}); | ||
} | ||
|
||
script.ast.body.push(methodStatement); | ||
|
||
// add localized message | ||
html.addFromRawHtml( | ||
template.ast.childNodes, | ||
`\n\n<h1>{m.hello_world({ name: 'SvelteKit User' })}</h1>\n` | ||
); | ||
|
||
// add links to other localized pages, the first one is the default | ||
// language, thus it does not require any localized route | ||
const { validLanguageTags } = parseLanguageTagInput(options.availableLanguageTags); | ||
const links = validLanguageTags | ||
.map((x) => `\n\t<button onclick="{() => switchToLanguage('${x}')}">${x}</button>`) | ||
.join(''); | ||
const div = html.element('div'); | ||
html.addFromRawHtml(div.childNodes, `${links}\n`); | ||
html.appendElement(template.ast.childNodes, div); | ||
|
||
return generateCode({ script: script.generateCode(), template: template.generateCode() }); | ||
} | ||
} | ||
], | ||
postInstall: ({ cwd, options }) => { | ||
const jsonData: Record<string, string> = {}; | ||
jsonData['$schema'] = 'https://inlang.com/schema/inlang-message-format'; | ||
|
||
const { validLanguageTags } = parseLanguageTagInput(options.availableLanguageTags); | ||
for (const languageTag of validLanguageTags) { | ||
jsonData.hello_world = `Hello, {name} from ${languageTag}!`; | ||
|
||
const filePath = `messages/${languageTag}.json`; | ||
const directoryPath = path.dirname(filePath); | ||
const fullDirectoryPath = path.join(cwd, directoryPath); | ||
const fullFilePath = path.join(cwd, filePath); | ||
|
||
fs.mkdirSync(fullDirectoryPath, { recursive: true }); | ||
fs.writeFileSync(fullFilePath, JSON.stringify(jsonData, null, 2) + '\n'); | ||
} | ||
}, | ||
nextSteps: ({ highlighter }) => [ | ||
`Edit your messages in ${highlighter.path('messages/en.json')}`, | ||
'Consider installing the Sherlock IDE Extension' | ||
] | ||
}); | ||
|
||
const isValidLanguageTag = (languageTag: string): boolean => | ||
// Regex vendored in from https://github.com/opral/monorepo/blob/94c2298cc1da5378b908e4c160b0fa71a45caadb/inlang/source-code/versioned-interfaces/language-tag/src/interface.ts#L16 | ||
RegExp( | ||
'^((?<grandfathered>(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?<language>([A-Za-z]{2,3}(-(?<extlang>[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?))(-(?<script>[A-Za-z]{4}))?(-(?<region>[A-Za-z]{2}|[0-9]{3}))?(-(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*))$' | ||
).test(languageTag); | ||
|
||
function parseLanguageTagInput(input: string): { | ||
validLanguageTags: string[]; | ||
invalidLanguageTags: string[]; | ||
} { | ||
const probablyLanguageTags = input | ||
.replace(/[,:\s]/g, ' ') // replace common separators with spaces | ||
.split(' ') | ||
.filter(Boolean) // remove empty segments | ||
.map((tag) => tag.toLowerCase()); | ||
|
||
const validLanguageTags: string[] = []; | ||
const invalidLanguageTags: string[] = []; | ||
|
||
for (const tag of probablyLanguageTags) { | ||
if (isValidLanguageTag(tag)) validLanguageTags.push(tag); | ||
else invalidLanguageTags.push(tag); | ||
} | ||
|
||
return { | ||
validLanguageTags, | ||
invalidLanguageTags | ||
}; | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { defineAdderTests } from '@sveltejs/cli-core'; | ||
import { options } from './index.ts'; | ||
// e2e tests make no sense in this context | ||
|
||
export const tests = defineAdderTests({ | ||
files: [], | ||
options, | ||
optionValues: [], | ||
tests: [] | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.