From c6581ecced7f6ec580f68ba5865fdcb3bb3031a6 Mon Sep 17 00:00:00 2001 From: Polle Pas Date: Mon, 29 Sep 2025 14:15:55 +0200 Subject: [PATCH] Add multilanguage support using Wuchale --- CONTRIBUTING.md | 10 +- browser/CHANGELOG.md | 1 + browser/CONTRIBUTING.md | 11 +- browser/data-browser/package.json | 6 +- browser/data-browser/src/Providers.tsx | 77 +- .../src/chunks/AI/AgentConfig.tsx | 2 +- .../src/chunks/AI/atomicSchemaHelpers.ts | 1 + .../src/chunks/AI/useAgentAutoSelect.ts | 6 +- .../src/chunks/AI/useAtomicTools.ts | 1 + .../src/chunks/AI/useGenerativeData.ts | 1 + .../src/chunks/AI/useProcessMessages.ts | 1 + .../CurrencyPicker/processCurrencyFile.ts | 1 + .../src/chunks/GraphViewer/getEdgeParams.ts | 1 + .../src/components/AI/AISidebarContext.tsx | 6 +- .../src/components/EditableTitle.tsx | 2 +- .../src/components/HotKeyWrapper.tsx | 1 + .../src/components/LocaleContext.tsx | 47 + .../components/Searchbar/SearchbarInput.tsx | 2 +- .../useTableEditorKeyboardNavigation.tsx | 4 +- .../components/Template/templates/website.tsx | 1 + .../src/components/forms/ResourceForm.tsx | 2 +- .../src/components/forms/UploadForm.tsx | 2 +- browser/data-browser/src/locales/de.po | 3040 ++++++++++++++++ browser/data-browser/src/locales/en.po | 3047 +++++++++++++++++ browser/data-browser/src/locales/es.po | 3015 ++++++++++++++++ browser/data-browser/src/locales/fr.po | 3035 ++++++++++++++++ browser/data-browser/src/locales/loader.ts | 37 + .../data-browser/src/routes/AppSettings.tsx | 25 + .../src/routes/NewResource/NewRoute.tsx | 2 +- browser/data-browser/src/routes/Sandbox.tsx | 1 + .../src/views/Article/ArticleCover.tsx | 14 +- .../CodeUsage/generators/CodeGenerator.ts | 1 + .../generators/TableCodeGenerator.ts | 1 + .../src/views/File/TextPreview.tsx | 2 +- .../EditorCells/MultiRelationCell.tsx | 2 +- .../src/views/TablePage/TableRow.tsx | 9 +- browser/data-browser/vite.config.ts | 2 + browser/data-browser/wuchale.config.js | 60 + browser/pnpm-lock.yaml | 142 +- browser/tsconfig.build.json | 2 +- 40 files changed, 12531 insertions(+), 92 deletions(-) create mode 100644 browser/data-browser/src/components/LocaleContext.tsx create mode 100644 browser/data-browser/src/locales/de.po create mode 100644 browser/data-browser/src/locales/en.po create mode 100644 browser/data-browser/src/locales/es.po create mode 100644 browser/data-browser/src/locales/fr.po create mode 100644 browser/data-browser/src/locales/loader.ts create mode 100644 browser/data-browser/wuchale.config.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66e9a978..9e35659e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,12 +7,13 @@ Same goes for feature requests. PR's are welcome, too! Note that opening a PR means agreeing that your code becomes distributed under the MIT license. -If you want to share some thoughts on the Atomic Data _specification_, please [drop an issue in the Atomic Data docs repo](https://github.com/ontola/atomic-data/issues). +If you want to share some thoughts on the Atomic Data _specification_, please [drop an issue in the Atomic Data repo](https://github.com/atomicdata-dev/atomic-server/issues). Check out the [Roadmap](https://docs.atomicdata.dev/roadmap.html) if you want to learn more about our plans and the history of the project. ## Table of contents - [Table of contents](#table-of-contents) +- [Translation \& Internationalization](#translation--internationalization) - [Running \& compiling](#running--compiling) - [Running locally (with local development browser)](#running-locally-with-local-development-browser) - [IDE setup (VSCode)](#ide-setup-vscode) @@ -39,6 +40,11 @@ Check out the [Roadmap](https://docs.atomicdata.dev/roadmap.html) if you want to - [Deploying to atomicdata.dev](#deploying-to-atomicdatadev) - [Publishing atomic-cli to WAPM](#publishing-atomic-cli-to-wapm) +## Translation & Internationalization + +AtomicServer supports a small number of languages. +Most of these translations are done by AI and might contain mistakes, if you notice any feel free to [open an issue](https://github.com/atomicdata-dev/atomic-server/issues). + ## Running & compiling TL;DR Clone the repo and run `cargo run` from each folder (e.g. `cli` or `server`). @@ -124,7 +130,7 @@ cargo nextest run test_name_substring # First, run the server cargo run # now, open new terminal window -cd server/e2e_tests/ && npm i && npm run test +cd browser && pnpm i && pnpm test-e2e # if things go wrong, debug! pnpm run test-query {testname} ``` diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index 969cdddf..aefeafd6 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -19,6 +19,7 @@ This changelog covers all five packages, as they are (for now) updated as a whol - [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1008) Add info dropdowns to different sections of the ontology editor for more information about the section. - [#459](https://github.com/atomicdata-dev/atomic-server/issues/459) New feature: Add tags to your resources to better organize your data. Search for resources with specific tags in the search bar with `tag:[name]`. - [#951](https://github.com/atomicdata-dev/atomic-server/issues/951) New feature: Atomic Assistant, AI chat interface with support for custom agents, MCP servers and more. Bring your own OpenRouter key or use Ollama to host your own models. +- [#1118](https://github.com/atomicdata-dev/atomic-server/issues/1118) New feature: AtomicServer is now also available in German, Spanish and French. Change your language on the settings page. ### @tomic/lib diff --git a/browser/CONTRIBUTING.md b/browser/CONTRIBUTING.md index 547587a0..db2c07b4 100644 --- a/browser/CONTRIBUTING.md +++ b/browser/CONTRIBUTING.md @@ -38,9 +38,12 @@ Vite hosts the data-browser and targets `.ts` files which enables hot reload / h If you're editing `@tomic/lib` or `@tomic/react`, you need to re-build the library, as `atomic-data-browser` imports the `.js` files. You can auto re-build using the `watch` commands in `@tomic/lib` and `@tomic/react`. If you run `pnpm start` from the root, these will be run automatically. -Note that you may need to refresh your screen manually to show updates from these libraries. -There are two possible solutions for improving this workflow: +## Localization -- In `package.json` of the libraries, set the `main` to `src/index.ts` (the typescript file). However, make sure to _not_ publish this to npm, as many clients will fail. -- Properly set up aliases with vite. I've tried this before, but failed. +Atomic Data Browser uses [Wuchale](https://wuchale.dev/) for localization. +When adding new text to the app wuchale will automatically extract it and add it to the locale files (When running the vite dev server). +Make sure you provide translations for the any new text you add. +To help with this you can provide a Google Gemini API key, Wuchale will then use this to generate translations for you automatically. +To do so export the key in your terminal or use something like direnv to set the key: `export GEMINI_API_KEY=your_api_key` +More info: [How to use Gemini live translation](https://wuchale.dev/guides/gemini/) diff --git a/browser/data-browser/package.json b/browser/data-browser/package.json index 115f9626..ba251d8d 100644 --- a/browser/data-browser/package.json +++ b/browser/data-browser/package.json @@ -64,6 +64,9 @@ "stylis": "4.3.0", "tippy.js": "^6.3.7", "tiptap-markdown": "^0.8.10", + "wuchale": "^0.16.5", + "@wuchale/jsx": "^0.7.4", + "@wuchale/vite-plugin": "^0.14.6", "zod": "^4.1.5" }, "devDependencies": { @@ -105,6 +108,7 @@ "preview": "vite preview", "start": "vite", "test": "vitest run", - "typecheck": "pnpm exec tsc --noEmit" + "typecheck": "pnpm exec tsc --noEmit", + "clean-translations": "pnpm exec wuchale --clean" } } diff --git a/browser/data-browser/src/Providers.tsx b/browser/data-browser/src/Providers.tsx index 130c23fe..276e1052 100644 --- a/browser/data-browser/src/Providers.tsx +++ b/browser/data-browser/src/Providers.tsx @@ -20,6 +20,7 @@ import { NavStateProvider } from './components/NavState'; import { Toaster } from './components/Toaster'; import { McpServersProvider } from './components/AI/MCP/useMcpServers'; import { AISettingsContextProvider } from '@components/AI/AISettingsContext'; +import { LocaleProvider } from '@components/LocaleContext'; // Setup bugsnag for error handling, but only if there's an API key const ErrBoundary = window.bugsnagApiKey @@ -45,43 +46,45 @@ const shouldForwardProp: ShouldForwardProp<'web'> = (propName, target) => { export const Providers: React.FC = ({ children }) => { return ( - - - - - - - - - - {/* Default form validation provider. Does not do anything on its own but will make sure useValidation works without context*/} - undefined} - > - - - - - - - - - {children} - - - - - - - - - - - - - - - + + + + + + + + + + + {/* Default form validation provider. Does not do anything on its own but will make sure useValidation works without context*/} + undefined} + > + + + + + + + + + {children} + + + + + + + + + + + + + + + + ); }; diff --git a/browser/data-browser/src/chunks/AI/AgentConfig.tsx b/browser/data-browser/src/chunks/AI/AgentConfig.tsx index e05ff2e6..1e9faa70 100644 --- a/browser/data-browser/src/chunks/AI/AgentConfig.tsx +++ b/browser/data-browser/src/chunks/AI/AgentConfig.tsx @@ -59,7 +59,7 @@ const defaultAgents: AIAgent[] = [ id: 'dev.atomicdata.atomic-agent', description: "An agent that is specialized in helping you use AtomicServer. It takes context from what you're doing.", - systemPrompt: `You are an AI assistant in the Atomic Data Browser. Users will ask questions about their data and you will answer by looking at the data or using your own knowledge about the world. + systemPrompt: /* wc-ignore */ `You are an AI assistant in the Atomic Data Browser. Users will ask questions about their data and you will answer by looking at the data or using your own knowledge about the world. Atomic Data uses JSON-AD, Every resource including the properties themselves have a subject (the '@id' property in the JSON-AD), this is a URL that points to the resource. Resources are always referenced by subject so make sure you have all the subjects you need before editing or creating resources. diff --git a/browser/data-browser/src/chunks/AI/atomicSchemaHelpers.ts b/browser/data-browser/src/chunks/AI/atomicSchemaHelpers.ts index 02038787..58d10665 100644 --- a/browser/data-browser/src/chunks/AI/atomicSchemaHelpers.ts +++ b/browser/data-browser/src/chunks/AI/atomicSchemaHelpers.ts @@ -1,3 +1,4 @@ +// @wc-ignore-file import { type Core, type Store } from '@tomic/react'; export const toClassString = async (subject: string, store: Store) => { diff --git a/browser/data-browser/src/chunks/AI/useAgentAutoSelect.ts b/browser/data-browser/src/chunks/AI/useAgentAutoSelect.ts index 64628c3e..249a7554 100644 --- a/browser/data-browser/src/chunks/AI/useAgentAutoSelect.ts +++ b/browser/data-browser/src/chunks/AI/useAgentAutoSelect.ts @@ -15,7 +15,7 @@ export const useAutoAgentSelect = () => { const getModel = useGetModel(); - const basePrompt = `You are part of an AI Chat application. It is your job to determine what agent to use to answer the users question. + const basePrompt = /* @wc-ignore */ `You are part of an AI Chat application. It is your job to determine what agent to use to answer the users question. These are the agents you can choose from: ${agents.map(agent => agentToText(agent, mcpServers)).join('\n')} @@ -35,8 +35,8 @@ User question: `; const { object } = await generateObject({ model, - schemaName: 'Agent', - schemaDescription: 'The agent to use for the question.', + schemaName: /* @wc-ignore */ 'Agent', + schemaDescription: /* @wc-ignore */ 'The agent to use for the question.', schema: z.object({ agentId: z.string(), }), diff --git a/browser/data-browser/src/chunks/AI/useAtomicTools.ts b/browser/data-browser/src/chunks/AI/useAtomicTools.ts index f1333433..460037af 100644 --- a/browser/data-browser/src/chunks/AI/useAtomicTools.ts +++ b/browser/data-browser/src/chunks/AI/useAtomicTools.ts @@ -1,3 +1,4 @@ +// @wc-ignore-file import { commits, core, diff --git a/browser/data-browser/src/chunks/AI/useGenerativeData.ts b/browser/data-browser/src/chunks/AI/useGenerativeData.ts index 29fa42fe..8b641e20 100644 --- a/browser/data-browser/src/chunks/AI/useGenerativeData.ts +++ b/browser/data-browser/src/chunks/AI/useGenerativeData.ts @@ -1,3 +1,4 @@ +// @wc-ignore-file import { generateObject, generateText } from 'ai'; import { type AtomicUIMessage } from './types'; import z from 'zod'; diff --git a/browser/data-browser/src/chunks/AI/useProcessMessages.ts b/browser/data-browser/src/chunks/AI/useProcessMessages.ts index b1c86879..a5a13861 100644 --- a/browser/data-browser/src/chunks/AI/useProcessMessages.ts +++ b/browser/data-browser/src/chunks/AI/useProcessMessages.ts @@ -1,3 +1,4 @@ +// @wc-ignore-file import { commits, useStore, type Resource, type Store } from '@tomic/react'; import { type AIMessageContext, type AtomicUIMessage } from './types'; import { toClassString } from './atomicSchemaHelpers'; diff --git a/browser/data-browser/src/chunks/CurrencyPicker/processCurrencyFile.ts b/browser/data-browser/src/chunks/CurrencyPicker/processCurrencyFile.ts index ab155355..dcb27bb1 100644 --- a/browser/data-browser/src/chunks/CurrencyPicker/processCurrencyFile.ts +++ b/browser/data-browser/src/chunks/CurrencyPicker/processCurrencyFile.ts @@ -1,3 +1,4 @@ +// @wc-ignore-file /** * Function to map currency codes to names using this list: https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml * Used to update the string in currencies.ts. diff --git a/browser/data-browser/src/chunks/GraphViewer/getEdgeParams.ts b/browser/data-browser/src/chunks/GraphViewer/getEdgeParams.ts index 86f81eb7..0905edb9 100644 --- a/browser/data-browser/src/chunks/GraphViewer/getEdgeParams.ts +++ b/browser/data-browser/src/chunks/GraphViewer/getEdgeParams.ts @@ -1,3 +1,4 @@ +// @wc-ignore-file import { Position, Node } from 'reactflow'; // this helper function returns the intersection point diff --git a/browser/data-browser/src/components/AI/AISidebarContext.tsx b/browser/data-browser/src/components/AI/AISidebarContext.tsx index c438896a..d21827c8 100644 --- a/browser/data-browser/src/components/AI/AISidebarContext.tsx +++ b/browser/data-browser/src/components/AI/AISidebarContext.tsx @@ -33,11 +33,11 @@ export const AISidebarContextProvider: React.FC = ({ ); }; -export const newContextItem = ( +export function newContextItem( item: Omit, -): T => { +): T { return { ...item, id: crypto.randomUUID() as string, } as T; -}; +} diff --git a/browser/data-browser/src/components/EditableTitle.tsx b/browser/data-browser/src/components/EditableTitle.tsx index d047bf7c..df282efd 100644 --- a/browser/data-browser/src/components/EditableTitle.tsx +++ b/browser/data-browser/src/components/EditableTitle.tsx @@ -58,7 +58,7 @@ export function EditableTitle({ setIsEditing(true); } - const placeholder = canEdit ? 'set a title' : 'Untitled'; + const placeholder = canEdit ? 'Set a title' : 'Untitled'; useEffect(() => { ref.current?.focus(); diff --git a/browser/data-browser/src/components/HotKeyWrapper.tsx b/browser/data-browser/src/components/HotKeyWrapper.tsx index d61cd4e1..c6d0dd5d 100644 --- a/browser/data-browser/src/components/HotKeyWrapper.tsx +++ b/browser/data-browser/src/components/HotKeyWrapper.tsx @@ -1,3 +1,4 @@ +// @wc-ignore-file import * as React from 'react'; import { dataURL, editURL } from '../helpers/navigation'; import { useHotkeys } from 'react-hotkeys-hook'; diff --git a/browser/data-browser/src/components/LocaleContext.tsx b/browser/data-browser/src/components/LocaleContext.tsx new file mode 100644 index 00000000..531affd8 --- /dev/null +++ b/browser/data-browser/src/components/LocaleContext.tsx @@ -0,0 +1,47 @@ +import { useLocalStorage } from '@hooks/useLocalStorage'; +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { loadLocale } from 'wuchale/load-utils'; + +interface LocaleContext { + locale: string; + setLocale: (locale: string) => void; +} + +const LocaleContext = createContext({ + locale: 'en', + setLocale: () => {}, +}); + +export const SUPPORTED_LOCALES = ['en', 'es', 'fr', 'de']; + +export const LocaleProvider = ({ children }: React.PropsWithChildren) => { + const [locale, setLocale] = useLocalStorage( + 'atomic.locale', + getBrowserLocale(), + ); + const [localeLoaded, setLocaleLoaded] = useState(false); + + useEffect(() => { + setLocaleLoaded(false); + loadLocale(locale).then(() => setLocaleLoaded(true)); + }, [locale]); + + return ( + + {/* Refresh the whole tree when changing locale */} + + {children} + + + ); +}; + +export const useLocale = () => { + return useContext(LocaleContext); +}; + +const getBrowserLocale = () => { + const locales = navigator.languages.map(x => x.trim().split(/-|_/)[0]); + + return locales.find(x => SUPPORTED_LOCALES.includes(x)) ?? 'en'; +}; diff --git a/browser/data-browser/src/components/Searchbar/SearchbarInput.tsx b/browser/data-browser/src/components/Searchbar/SearchbarInput.tsx index 0ac592cc..8e303027 100644 --- a/browser/data-browser/src/components/Searchbar/SearchbarInput.tsx +++ b/browser/data-browser/src/components/Searchbar/SearchbarInput.tsx @@ -40,7 +40,7 @@ function useTagList(): TagWithTitle[] { // Gracefully fall back to a no-op implementation if the browser doesn't support the Highlight API. const newHighlight = () => { - if ('Highlight' in window) { + if (/* @wc-ignore */ 'Highlight' in window) { return new window.Highlight(); } diff --git a/browser/data-browser/src/components/TableEditor/hooks/useTableEditorKeyboardNavigation.tsx b/browser/data-browser/src/components/TableEditor/hooks/useTableEditorKeyboardNavigation.tsx index 10a6fd6d..00378906 100644 --- a/browser/data-browser/src/components/TableEditor/hooks/useTableEditorKeyboardNavigation.tsx +++ b/browser/data-browser/src/components/TableEditor/hooks/useTableEditorKeyboardNavigation.tsx @@ -19,7 +19,9 @@ const matchModifier = ( ) => handler.mod === undefined || handler.mod === - (navigator.platform.includes('Mac') ? event.metaKey : event.ctrlKey); + (navigator.platform.includes(/* @wc-ignore */ 'Mac') + ? event.metaKey + : event.ctrlKey); const matchCondition = (handler: KeyboardHandler, context: HandlerContext) => handler.condition === undefined || handler.condition(context); diff --git a/browser/data-browser/src/components/Template/templates/website.tsx b/browser/data-browser/src/components/Template/templates/website.tsx index a03fbf93..431cc7a2 100644 --- a/browser/data-browser/src/components/Template/templates/website.tsx +++ b/browser/data-browser/src/components/Template/templates/website.tsx @@ -1,3 +1,4 @@ +// @wc-ignore-file import { core, dataBrowser } from '@tomic/react'; import type { TemplateFn, TemplateContext } from '../template'; diff --git a/browser/data-browser/src/components/forms/ResourceForm.tsx b/browser/data-browser/src/components/forms/ResourceForm.tsx index a9745f30..ce53fa5c 100644 --- a/browser/data-browser/src/components/forms/ResourceForm.tsx +++ b/browser/data-browser/src/components/forms/ResourceForm.tsx @@ -251,7 +251,7 @@ export function ResourceForm({
diff --git a/browser/data-browser/src/components/forms/UploadForm.tsx b/browser/data-browser/src/components/forms/UploadForm.tsx index 188a869f..97ff7672 100644 --- a/browser/data-browser/src/components/forms/UploadForm.tsx +++ b/browser/data-browser/src/components/forms/UploadForm.tsx @@ -41,7 +41,7 @@ export default function UploadForm({
{isDragActive ? ( -

{'Drop the files here ...'}

+

Drop the files here ...

) : (