diff --git a/package.json b/package.json index 54e378c2..35172ff1 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@sveltejs/vite-plugin-svelte": "^3.1.2", "@types/escape-html": "^1.0.4", "@types/glidejs__glide": "^3.6.6", + "@types/js-cookie": "^3.0.6", "@types/minimist": "^1.2.5", "@types/node": "^20.17.31", "@types/remark-heading-id": "^1.0.0", @@ -84,6 +85,7 @@ "escape-html": "^1.0.3", "github-slugger": "^2.0.0", "html-to-image": "^1.11.13", + "js-cookie": "^3.0.5", "lucide-svelte": "^0.325.0", "maplibre-gl": "^5.4.0", "maplibregl-mapbox-request-transformer": "^0.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c922ec33..eeeae3b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: html-to-image: specifier: ^1.11.13 version: 1.11.13 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 lucide-svelte: specifier: ^0.325.0 version: 0.325.0(svelte@4.2.19) @@ -117,6 +120,9 @@ importers: '@types/glidejs__glide': specifier: ^3.6.6 version: 3.6.6 + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 '@types/minimist': specifier: ^1.2.5 version: 1.2.5 @@ -960,6 +966,9 @@ packages: '@types/hast@2.3.10': resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1804,6 +1813,10 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-sha256@0.11.0: resolution: {integrity: sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==} @@ -3323,6 +3336,8 @@ snapshots: dependencies: '@types/unist': 2.0.11 + '@types/js-cookie@3.0.6': {} + '@types/json-schema@7.0.15': {} '@types/mapbox-gl@2.7.21': @@ -4202,6 +4217,8 @@ snapshots: isexe@3.1.1: {} + js-cookie@3.0.5: {} + js-sha256@0.11.0: {} js-yaml@4.1.0: diff --git a/project.inlang/default-settings.js b/project.inlang/default-settings.js index 78f60682..8a1173fc 100644 --- a/project.inlang/default-settings.js +++ b/project.inlang/default-settings.js @@ -13,11 +13,11 @@ export default { 'https://cdn.jsdelivr.net/npm/@inlang/plugin-paraglide-js-adapter@latest/dist/index.js' ], 'plugin.inlang.messageFormat': { - pathPattern: './messages/{locale}.json' + pathPattern: './src/temp/translations/json/{locale}.json' }, 'plugin.paraglide-js-adapter': { routing: { - strategy: 'prefix', + strategy: 'prefix-all-locales', defaultLocale: 'en' } } diff --git a/scripts/git-touch.sh b/scripts/git-touch.sh new file mode 100755 index 00000000..945e718b --- /dev/null +++ b/scripts/git-touch.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# Check if a file was provided +if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +FILE="$1" + +# Check if the file exists +if [ ! -f "$FILE" ]; then + echo "Error: File '$FILE' not found." + exit 1 +fi + +# Check if the file has YAML frontmatter (starts with ---) +if ! grep -q "^---" "$FILE"; then + echo "Error: File does not have YAML frontmatter." + exit 1 +fi + +# Get current timestamp +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +# Find the first two occurrences of --- in the file to identify the frontmatter section +FIRST_LINE=$(grep -n "^---" "$FILE" | head -1 | cut -d':' -f1) +SECOND_LINE=$(grep -n "^---" "$FILE" | head -2 | tail -1 | cut -d':' -f1) + +if [ -z "$FIRST_LINE" ] || [ -z "$SECOND_LINE" ] || [ "$FIRST_LINE" -eq "$SECOND_LINE" ]; then + echo "Error: Could not properly identify YAML frontmatter boundaries." + exit 1 +fi + +# Create a temporary file for processing +TMP_FILE=$(mktemp) + +# Extract the frontmatter, modify it, then reconstruct the file +head -n "$FIRST_LINE" "$FILE" > "$TMP_FILE" +sed -n "$((FIRST_LINE+1)),$((SECOND_LINE-1))p" "$FILE" > "$TMP_FILE.frontmatter" + +# Check if git-touch already exists in frontmatter +if grep -q "git-touch:" "$TMP_FILE.frontmatter"; then + # Update the git-touch line + sed -i "s/^git-touch:.*$/git-touch: $TIMESTAMP/" "$TMP_FILE.frontmatter" +else + # Add git-touch as a new line + echo "git-touch: $TIMESTAMP" >> "$TMP_FILE.frontmatter" +fi + +# Append modified frontmatter to the temp file +cat "$TMP_FILE.frontmatter" >> "$TMP_FILE" +echo "---" >> "$TMP_FILE" + +# Append the rest of the original file after the frontmatter +tail -n +$((SECOND_LINE+1)) "$FILE" >> "$TMP_FILE" + +# Replace original with modified file +mv "$TMP_FILE" "$FILE" +rm -f "$TMP_FILE.frontmatter" + +# Stage the file +git add "$FILE" + +# Commit with message +git commit -m "git-touch $(basename "$FILE")" + +echo "Successfully touched and committed $(basename "$FILE")" \ No newline at end of file diff --git a/scripts/git-untouch.sh b/scripts/git-untouch.sh new file mode 100755 index 00000000..314bd6bd --- /dev/null +++ b/scripts/git-untouch.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Script to remove the git-touch field from markdown frontmatter in posts and translations +# Usage: ./git-untouch.sh + +set -e + +echo "Removing git-touch from markdown files..." + +# Process main posts directory +echo "Processing src/posts/*.md" +find src/posts -name "*.md" | xargs sed -i '/^git-touch:/d' + +# Process translation directories +echo "Processing src/temp/translations/md/*/*.md" +find src/temp/translations/md -path "src/temp/translations/md/*/*.md" | xargs sed -i '/^git-touch:/d' + +echo "Complete! Git-touch fields have been removed." \ No newline at end of file diff --git a/scripts/inlang-settings.ts b/scripts/inlang-settings.ts index 8fb4dcfa..4bd0cf74 100644 --- a/scripts/inlang-settings.ts +++ b/scripts/inlang-settings.ts @@ -12,26 +12,11 @@ import { writeSettingsFile } from '../src/lib/l10n' import { setupTranslationRepo } from './translation/git-ops' -import { createSymlinkIfNeeded, ensureDirectoriesExist } from './translation/utils' +import { cullCommentary, createSymlinkIfNeeded, ensureDirectoriesExist } from './translation/utils' // Load environment variables from .env file dotenv.config() -// Configuration - same as in vite.config.ts -const PROJECT_PATH = './project.inlang' -const OUTPUT_PATH = './src/lib/paraglide' -const COMPILE_ARGS = { - project: PROJECT_PATH, - outdir: OUTPUT_PATH, - strategy: ['cookie', 'url', 'preferredLanguage', 'baseLocale'] as ( - | 'cookie' - | 'url' - | 'preferredLanguage' - | 'baseLocale' - | 'globalVariable' - | 'localStorage' - )[] -} function setupEnglishSupport(verbose: boolean): void { // English markdown files are loaded directly from source in routes/[slug]/+page.ts if (verbose) console.log(' \u2713 English markdown files will be loaded directly from source') @@ -93,6 +78,11 @@ function regenerateSettings(verbose = false): void { `\n\ud83d\udd04 Setting up translation repository (need at least ${settings.locales}...` ) setupTranslationRepo(L10NS_BASE_DIR, verbose) + if (verbose) console.log(`\n🧹 Cleaning up translation files to remove LLM commentary...`) + for (const locale of settings.locales) { + if (locale === 'en') continue + cullCommentary(path.join(MESSAGE_L10NS, `${locale}.json`), verbose) + } } // For English locale, we only need to provide messages file for Paraglide @@ -118,8 +108,21 @@ function regenerateSettings(verbose = false): void { console.log(`\ud83d\udd04 Compiling Paraglide runtime from settings...`) try { - // Run the Paraglide compiler with the necessary Node.js flags - compile(COMPILE_ARGS) + compile({ + project: './project.inlang', + outdir: './src/lib/paraglide', + strategy: ['url', 'cookie', 'preferredLanguage', 'baseLocale'], + // Create concrete URL patterns structure with current locale set + urlPatterns: [ + { + pattern: ':protocol://:domain(.*)::port?/:path(.*)?', + localized: settings.locales.map((locale) => [ + locale, + `:protocol://:domain(.*)::port?/${locale}/:path(.*)?` + ]) + } + ] + }) console.log(`\u2705 Paraglide runtime compiled successfully!`) } catch (error) { console.error('\u274c Failed to compile Paraglide runtime:', (error as Error).message) diff --git a/scripts/translation/git-ops.ts b/scripts/translation/git-ops.ts index e5ce4cf7..f04ffb94 100644 --- a/scripts/translation/git-ops.ts +++ b/scripts/translation/git-ops.ts @@ -111,9 +111,24 @@ export async function initializeGitCache(options: { console.log( `\ud83d\udd04 Setting up translation repository from ${options.repo} into ${options.dir}` ) + console.log(`Using ${options.token ? 'authenticated' : 'unauthenticated'} Git access`) + + // Time the clone operation + console.time('ā±ļø Git Clone') await options.git.clone(remote, options.dir) + console.timeEnd('ā±ļø Git Clone') + await options.git.cwd(options.dir) + // Test if we're authenticated by checking remote URL format + try { + const remoteUrl = await options.git.remote(['get-url', 'origin']) + const isAuthenticated = remoteUrl.includes('@') + console.log(`Authentication status: ${isAuthenticated ? 'SUCCESS' : 'FAILURE'}`) + } catch (err) { + console.log(`Failed to verify authentication: ${err.message}`) + } + // Always set git config in case we need to make local commits await options.git.addConfig('user.name', options.username) await options.git.addConfig('user.email', options.email) @@ -125,11 +140,24 @@ export async function initializeGitCache(options: { * @param git - The SimpleGit instance used to retrieve the log. * @returns A Promise that resolves to a Map where keys are file paths and values are the latest commit dates. */ -export async function getLatestCommitDates(git: SimpleGit): Promise> { +export async function getLatestCommitDates( + git: SimpleGit, + repoType: string = 'repo' +): Promise> { + console.log(`Starting git log retrieval for ${repoType} commit dates...`) const latestCommitDatesMap = new Map() + + const timerLabelLog = `ā±ļø Git Log Retrieval - ${repoType}` + console.time(timerLabelLog) const log = await git.log({ '--stat': 4096 }) + console.timeEnd(timerLabelLog) + + console.log(`Retrieved ${log.all.length} commits for ${repoType} date analysis`) + + const timerLabelParse = `ā±ļø Parse Git Log - ${repoType}` + console.time(timerLabelParse) for (const entry of log.all) { const files = entry.diff?.files if (!files) continue @@ -139,6 +167,9 @@ export async function getLatestCommitDates(git: SimpleGit): Promise { return `${commonHead(languageName, 'Markdown', promptAdditions)} -Maintain the structure of the document and don't modify script elements, HTML tags, link targets (including section links starting with a number sign) and file names. The content of social media widgets has to remain unchanged as well. I repeat: DO NOT CHANGE LINK TARGETS. DO NOT REMOVE SCRIPT TAGS OR COMPONENTS. The document starts with Frontmatter metadata enclosed in three dashes. Don't translate the keys of the metadata, only the values. Always make sure the result is valid Markdown syntax: +Maintain the structure of the document. Don't modify script elements, HTML tags, link targets (including section links starting with a number sign) and file names. The content of social media widgets has to remain unchanged as well. I repeat: DO NOT CHANGE LINK TARGETS. DO NOT REMOVE SCRIPT TAGS OR COMPONENTS. The document starts with Frontmatter metadata enclosed in three dashes. Don't translate the keys of the metadata, only the values. Always make sure the result is valid Markdown syntax: ${content} Do not start with \`\`\`md or new lines, just return the Markdown. + Translated Markdown:` } export function generateReviewPrompt(languageName: string) { - return `Please review and improve your translation to ${languageName} to ensure it meets the highest standards of quality. + return `Please review and (only if necessary) improve your translation to ${languageName} to ensure it meets the highest standards of quality. Focus on: 1. Accuracy of meaning compared to typical English expressions @@ -45,12 +46,16 @@ Focus on: 5. Maintaining the original formatting and technical elements Make necessary improvements while preserving: -- All technical elements (HTML tags, links, script elements) +- All technical elements (keys, HTML tags, links, script elements) - Document structure and formatting - Special instructions in comments - File names and technical terms when appropriate -Return the improved version in the same format, without any additional markup or explanations.` +Return the possibly improved text in the same format. + +All of your response will be used to generate website pages. Readers don't want to see your own translation notes. Do NOT tell us that you have done the work, or why particular decisions were made. That would make your response invalid. Only return the translation itself. + +Translation:` } function commonHead(languageName: string, format: string, promptAdditions: string) { diff --git a/scripts/translation/translate-core.ts b/scripts/translation/translate-core.ts index 82ebf661..5957c6a1 100644 --- a/scripts/translation/translate-core.ts +++ b/scripts/translation/translate-core.ts @@ -50,7 +50,7 @@ export interface TranslationOptions { * or collects statistics in dry run mode without making API calls. * * @param content - The original content to be translated. - * @param promptGenerator - A function for generating the translation prompt. + * @param promptGenerators - Functions for generating the translation and review prompt. * @param language - The target language code. * @param promptAdditions - Additional context to include in the prompt. * @param options - Translation configuration options. @@ -60,7 +60,7 @@ export interface TranslationOptions { */ export async function translate( content: string, - promptGenerator: PromptGenerator, + promptGenerators: PromptGenerator[], language: string, promptAdditions: string, options: TranslationOptions, @@ -69,7 +69,8 @@ export async function translate( const languageName = options.languageNameGenerator.of(language) if (!languageName) throw new Error(`Couldn't resolve language code: ${language}`) - const translationPrompt = promptGenerator(languageName, content, promptAdditions) + const translationPrompt = promptGenerators[0](languageName, content, promptAdditions) + // Translation prompt ready // In dry run mode, collect statistics instead of making API calls if (options.isDryRun) { @@ -95,6 +96,7 @@ export async function translate( if (!firstPass) throw new Error(`Translation to ${languageName} failed`) if (options.verbose) { + console.log('First prompt: ', translationPrompt) console.log('First pass response:', firstPass) } else { console.log( @@ -102,9 +104,8 @@ export async function translate( ) } - // Second pass: review and refine translation with context - const reviewPrompt = `Please review your translation to ${languageName} for accuracy and naturalness. Make improvements where necessary but keep the meaning identical to the source.` - + // Secon d pass: review and refine translation with context + const reviewPrompt = promptGenerators[1](languageName) const reviewed = await postChatCompletion(options.llmClient, options.requestQueue, [ { role: 'user', content: translationPrompt }, { role: 'assistant', content: firstPass }, @@ -114,6 +115,7 @@ export async function translate( if (!reviewed) throw new Error(`Review of ${languageName} translation failed`) if (options.verbose) { + console.log('Review prompt: ', reviewPrompt) console.log('Review pass response:', reviewed) } else { console.log( @@ -136,7 +138,7 @@ export async function translateOrLoadMessages( options: { sourcePath: string languageTags: string[] - promptGenerator: PromptGenerator + promptGenerators: PromptGenerator[] targetDir: string cacheGitCwd: string logMessageFn?: (msg: string) => void @@ -147,7 +149,7 @@ export async function translateOrLoadMessages( { sourcePaths: [options.sourcePath], languageTags: options.languageTags, - promptGenerator: options.promptGenerator, + promptGenerators: options.promptGenerators, targetStrategy: (language) => path.join(options.targetDir, language + '.json'), cacheGitCwd: options.cacheGitCwd, logMessageFn: options.logMessageFn @@ -170,7 +172,7 @@ export async function translateOrLoadMarkdown( sourcePaths: string[] sourceBaseDir: string languageTags: string[] - promptGenerator: PromptGenerator + promptGenerators: PromptGenerator[] targetDir: string cacheGitCwd: string logMessageFn?: (msg: string) => void @@ -181,7 +183,7 @@ export async function translateOrLoadMarkdown( { sourcePaths: options.sourcePaths, languageTags: options.languageTags, - promptGenerator: options.promptGenerator, + promptGenerators: options.promptGenerators, targetStrategy: (language, sourcePath) => { const relativePath = path.relative(options.sourceBaseDir, sourcePath) return path.join(options.targetDir, language, relativePath) @@ -206,7 +208,7 @@ export async function translateOrLoad( options: { sourcePaths: string[] languageTags: string[] - promptGenerator: PromptGenerator + promptGenerators: PromptGenerator[] targetStrategy: TargetStrategy cacheGitCwd: string logMessageFn?: (msg: string) => void @@ -214,6 +216,7 @@ export async function translateOrLoad( translationOptions: TranslationOptions ): Promise<{ cacheCount: number; totalProcessed: number }> { const log = options.logMessageFn || console.log + // Log function ready let done = 1 let total = 0 let cacheCount = 0 @@ -267,7 +270,7 @@ export async function translateOrLoad( const translation = await translate( processedContent, - options.promptGenerator, + options.promptGenerators, languageTag, promptAdditions, translationOptions, diff --git a/scripts/translation/translate.ts b/scripts/translation/translate.ts index ef0a4789..252fd225 100644 --- a/scripts/translation/translate.ts +++ b/scripts/translation/translate.ts @@ -20,7 +20,7 @@ import { initializeGitCache } from './git-ops' import { createLlmClient, createRequestQueue, LLM_DEFAULTS } from './llm-client' -import { generateJsonPrompt, generateMarkdownPrompt } from './prompts' +import { generateJsonPrompt, generateMarkdownPrompt, generateReviewPrompt } from './prompts' import { translateOrLoadMarkdown, translateOrLoadMessages } from './translate-core' import { requireEnvVar } from './utils' @@ -158,20 +158,21 @@ const languageNamesInEnglish = new Intl.DisplayNames('en', { type: 'language' }) email: GIT_CONFIG.EMAIL, git: cacheGit }) - translationOptions.cacheLatestCommitDates = await getLatestCommitDates(cacheGit) + translationOptions.cacheLatestCommitDates = await getLatestCommitDates(cacheGit, 'cache') })(), (async () => - (translationOptions.mainLatestCommitDates = await getLatestCommitDates(mainGit)))() + (translationOptions.mainLatestCommitDates = await getLatestCommitDates(mainGit, 'main')))() ]) // Process both message files and markdown files in parallel + // Begin message translation const results = await Promise.all([ (async () => { const result = await translateOrLoadMessages( { sourcePath: MESSAGE_SOURCE, languageTags: languageTags, - promptGenerator: generateJsonPrompt, + promptGenerators: [generateJsonPrompt, generateReviewPrompt], targetDir: MESSAGE_L10NS, cacheGitCwd: L10NS_BASE_DIR, logMessageFn: logMessage @@ -196,7 +197,7 @@ const languageNamesInEnglish = new Intl.DisplayNames('en', { type: 'language' }) sourcePaths: markdownPathsFromRoot, sourceBaseDir: MARKDOWN_SOURCE, languageTags: languageTags, - promptGenerator: generateMarkdownPrompt, + promptGenerators: [generateMarkdownPrompt, generateReviewPrompt], targetDir: MARKDOWN_L10NS, cacheGitCwd: L10NS_BASE_DIR, logMessageFn: logMessage @@ -221,6 +222,11 @@ const languageNamesInEnglish = new Intl.DisplayNames('en', { type: 'language' }) console.log(` - ${newTranslations} files needed new translations`) // Only push to Git if we actually created new translations + + // **We need to remove use of a single mainline for the repos very soon!** + // Currently developers can touch the repos easily, as can previews for pull requests etc. + // Any subsequent production deploy would show use the altereed in-development translations to viewers + if (newTranslations > 0) { console.log(`\nPushing translation changes to repository...`) await cacheGit.push() diff --git a/scripts/translation/utils.ts b/scripts/translation/utils.ts index adc06dae..6bce9469 100644 --- a/scripts/translation/utils.ts +++ b/scripts/translation/utils.ts @@ -237,3 +237,29 @@ export async function writeFileWithDir(filePath: string, content: string): Promi await fs.mkdir(dir, { recursive: true }) fsSync.writeFileSync(filePath, content) } + +/** + * Cleans up potential LLM commentary in translation JSON files + * Strips anything before the first '{' and after the last '}' + * + * @param filePath - Path to the JSON file to clean + * @param verbose - Whether to output verbose logs + */ +export function cullCommentary(filePath: string, verbose = false): boolean { + try { + const content = fsSync.readFileSync(filePath, 'utf-8') + const firstBrace = content.indexOf('{') + const lastBrace = content.lastIndexOf('}') + + if (firstBrace === -1 || lastBrace === -1) throw new Error('No JSON object found in file') + + const jsonContent = content.substring(firstBrace, lastBrace + 1) + JSON.parse(jsonContent) // checks validity + if (jsonContent === content) return + fsSync.writeFileSync(filePath, jsonContent, 'utf-8') + + if (verbose) console.log(`āœ… Culled LLM commentary in ${filePath}`) + } catch (error) { + console.error(`Error cleaning up file ${filePath}:`, error.message) + } +} diff --git a/src/lib/adapter-patch-prerendered.js b/src/lib/adapter-patch-prerendered.js index 329d0f7d..d278d2b3 100644 --- a/src/lib/adapter-patch-prerendered.js +++ b/src/lib/adapter-patch-prerendered.js @@ -11,11 +11,11 @@ export default function (adapter) { /** * @type {import('../../project.inlang/settings.json')} */ - const settings = JSON.parse(fs.readFileSync('./project.inlang/settings.json', 'utf-8')) + //const settings = JSON.parse(fs.readFileSync('./project.inlang/settings.json', 'utf-8')) //builder.prerendered.paths = builder.prerendered.paths.filter((path) => { - //for (const locale of settings.locales) { - //if (path.startsWith('/' + locale)) return true - //} + // for (const locale of settings.locales) { + // if (path.startsWith('/' + locale)) return true + // } //}) adapter.adapt(builder) } diff --git a/src/lib/components/LanguageSwitcher.svelte b/src/lib/components/LanguageSwitcher.svelte index df54f560..9e8ab420 100644 --- a/src/lib/components/LanguageSwitcher.svelte +++ b/src/lib/components/LanguageSwitcher.svelte @@ -13,12 +13,16 @@ import { building } from '$app/environment' import { onMount } from 'svelte' import Card from '$lib/components/Card.svelte' + import Cookies from 'js-cookie' export let inverted = false // Check if we should show the language switcher (only show when multiple locales) const showSwitcher = locales.length > 1 + // Flag to track if locale was changed externally + let externalLocaleChange = false + // Display name utilities for multiple locales const languageNamesInEnglish = new Intl.DisplayNames('en', { type: 'language' }) // Keep a map of locale-specific display names @@ -64,31 +68,35 @@ return `${currentLocaleName} (${nativeName})` } - // Display the current locale state in UI and console for debugging - // TODO: disable this once everything is confirmed to work well in production - function debugLocaleState() { - // Get current values - const currentLocale = getLocale() - const cookieLocale = document.cookie - .split('; ') - .find((row) => row.startsWith('PARAGLIDE_LOCALE=')) - ?.split('=')[1] - const pathLocale = window.location.pathname.split('/')[1] - const isPathLocale = locales.includes(pathLocale as any) - - console.log('šŸ” Locale Debug:', { - getLocale: currentLocale, - cookieLocale, - pathLocale: isPathLocale ? pathLocale : null, - allLocales: locales - }) - - return currentLocale + // Check if cookie was changed externally (and update shadow cookie which tracks this) + function checkLocaleChange(): boolean { + if (typeof window === 'undefined') return false + + const cookieLocale = Cookies.get('PARAGLIDE_LOCALE') + const shadowLocale = Cookies.get('PARAGLIDE_LOCALE_SHADOW') + + // Check for change - treat undefined shadow cookie as a change too + // (This catches first-time visits where shadow cookie isn't set yet) + const wasChanged = shadowLocale !== cookieLocale + + // Always update the shadow cookie to match the current value + if (cookieLocale) { + Cookies.set('PARAGLIDE_LOCALE_SHADOW', cookieLocale, { path: '/' }) + } else { + Cookies.remove('PARAGLIDE_LOCALE_SHADOW', { path: '/' }) + } + + return wasChanged } onMount(() => { - // Debug on mount - debugLocaleState() + if (typeof window === 'undefined') return + + if (checkLocaleChange()) { + // Add visual indication + console.log('🌐 Locale was changed externally!') + if (button) button.classList.add('locale-changed') + } // Only set up event listeners if language switcher is visible if (showSwitcher) { @@ -111,16 +119,6 @@ const href = target.href const targetLocale = target.getAttribute('hreflang') as Locale | 'auto' - // Debug current state before change - console.log('šŸ” Before click:') - const currentLocale = debugLocaleState() - - console.log('Language click:', { - from: currentLocale, - to: targetLocale, - href: href - }) - // Close the dropdown open = false @@ -128,14 +126,17 @@ event.preventDefault() if (targetLocale === 'auto') { - document.cookie = 'PARAGLIDE_LOCALE=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' - document.cookie = 'PARAGLIDE_LOCALE=; path=/; max-age=0' + // Remove both cookies for auto-detect + Cookies.remove('PARAGLIDE_LOCALE', { path: '/' }) + Cookies.remove('PARAGLIDE_LOCALE_SHADOW', { path: '/' }) window.location.href = deLocalizeHref(window.location.pathname) return } // Handle regular locale selection if (targetLocale) { + // Update shadow cookie to prevent detection as an external change + Cookies.set('PARAGLIDE_LOCALE_SHADOW', targetLocale, { path: '/' }) // explicit reload seems necessary to interact correctly with SvelteKit client-side refresh setLocale(targetLocale, { reload: true }) // As a fallback, manually reload if we're still here after a short delay @@ -155,6 +156,7 @@ bind:this={button} on:click={(e) => { e.preventDefault() + button.classList.remove('locale-changed') // user has attended open = !open }} > @@ -254,4 +256,25 @@ visibility: hidden; pointer-events: none; } + + /* Visual indicator for locale change */ + :global(.locale-changed) { + animation: pulse 2s infinite; + transform-origin: center; + } + + @keyframes pulse { + 0% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.6; + transform: scale(1.2); + } + 100% { + opacity: 1; + transform: scale(1); + } + } diff --git a/src/posts/learn.md b/src/posts/learn.md index f35e52a9..187435cd 100644 --- a/src/posts/learn.md +++ b/src/posts/learn.md @@ -98,7 +98,7 @@ I haven't heard good things about it yet --> - [If Anyone Builds It, Everyone Dies](https://ifanyonebuildsit.com/) (Eliezer Yudkowsky & Nate Soares, 2025) - [Uncontrollable: The Threat of Artificial Superintelligence and the Race to Save the World](https://www.goodreads.com/book/show/202416160-uncontrollable) (Darren McKee, 2023). Get it for [free](https://impactbooks.store/cart/47288196366640:1?discount=UNCON-P3SFRS)! +I love this book, but just a fraction of it is about AI --> - [The Alignment Problem](https://www.goodreads.com/book/show/50489349-the-alignment-problem) (Brian Christian, 2020) - [Human Compatible: Artificial Intelligence and the Problem of Control](https://www.goodreads.com/en/book/show/44767248) (Stuart Russell, 2019) - [Life 3.0: Being Human in the Age of Artificial Intelligence](https://www.goodreads.com/en/book/show/34272565) (Max Tegmark, 2017) diff --git a/src/posts/legal.md b/src/posts/legal.md index 02fb66f6..d85c01aa 100644 --- a/src/posts/legal.md +++ b/src/posts/legal.md @@ -2,6 +2,7 @@ title: Legal information description: Information required by law about PauseAI. --- + This website is maintained by PauseAI Global. Read more about the inner workings about PauseAI on the [Organization page](/organization). diff --git a/src/posts/values.md b/src/posts/values.md index 470c711a..dc80787d 100644 --- a/src/posts/values.md +++ b/src/posts/values.md @@ -2,6 +2,7 @@ title: PauseAI values description: How does PauseAI plan to achieve its mission? --- + ## What do we want? Globally halt frontier AI development until we know how to do it safely and under democratic control. See our [proposal](/proposal). diff --git a/src/routes/header.svelte b/src/routes/header.svelte index 2e493dbc..d6444363 100644 --- a/src/routes/header.svelte +++ b/src/routes/header.svelte @@ -15,7 +15,7 @@ export let inverted = false export let moveUp = false - $: logo_animate = localizeHref($page.url.pathname) != '/' + $: logo_animate = $page.url.pathname != localizeHref('/') let nav: HTMLElement @@ -30,7 +30,7 @@
-
diff --git a/src/routes/sitemap.txt/+server.ts b/src/routes/sitemap.txt/+server.ts index 6893856c..d19fa75e 100644 --- a/src/routes/sitemap.txt/+server.ts +++ b/src/routes/sitemap.txt/+server.ts @@ -11,7 +11,7 @@ export async function GET({ fetch }) { const headers = { 'Content-Type': 'text/plain' } const sitemap = posts - .map(({ slug }) => `${website}/${slug}\n`) + .map(({ slug }) => `${website}/en/${slug}\n`) .join('') .trim() diff --git a/src/routes/sitemap.xml/+server.ts b/src/routes/sitemap.xml/+server.ts index 5bbce255..f3e8bb1b 100644 --- a/src/routes/sitemap.xml/+server.ts +++ b/src/routes/sitemap.xml/+server.ts @@ -21,7 +21,7 @@ export async function GET({ fetch }) { xmlns:video="https://www.google.com/schemas/sitemap-video/1.1" > - ${website} + ${website}/en daily 0.7 @@ -29,7 +29,7 @@ export async function GET({ fetch }) { .map( (post) => ` - ${website}/${post.slug} + ${website}/en/${post.slug} daily 0.7 `