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 66e9a9786..9e35659ea 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 969cdddff..aefeafd62 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 547587a0c..db2c07b46 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 115f96264..ba251d8d2 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 130c23fe2..276e1052f 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 e05ff2e60..1e9faa70b 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 02038787e..58d106656 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 64628c3e1..249a75543 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 f1333433d..460037afc 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 29fa42fea..8b641e209 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 b1c868790..a5a138616 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 ab1553559..dcb27bb12 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 86f81eb79..0905edb90 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 c438896a8..d21827c80 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 d047bf7c5..df282efd1 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 d61cd4e12..c6d0dd5d0 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 000000000..531affd8f --- /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 0ac592cc2..8e303027d 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 10a6fd6d9..00378906b 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 a03fbf936..431cc7a23 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 a9745f30b..ce53fa5cd 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 188a869fa..97ff76720 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 ...

) : (