From 55fbcd0d7ea6a1349002dc775cd361924796e96b Mon Sep 17 00:00:00 2001 From: Esa Komulainen Date: Sun, 18 Oct 2026 11:03:36 +0300 Subject: [PATCH 01/12] M10: add Finnish (fi) interface catalog --- crates/ui-egui/locales/fi.toml | 2343 ++++++++++++++++++++++++++++++ crates/ui-egui/src/i18n_tests.rs | 40 + docs/translating.md | 12 + 3 files changed, 2395 insertions(+) create mode 100644 crates/ui-egui/locales/fi.toml diff --git a/crates/ui-egui/locales/fi.toml b/crates/ui-egui/locales/fi.toml new file mode 100644 index 00000000..eb3ce94c --- /dev/null +++ b/crates/ui-egui/locales/fi.toml @@ -0,0 +1,2343 @@ +# Finnish interface catalog. Original translations; MIT OR Apache-2.0. +# AI-assisted draft; not reviewed by a human Finnish/native speaker. +# Sentence case, imperative commands. Integer plurals: 1 taso, 0/2/21 tasoa. +# Product names, shortcut notation, numeric glyphs and code examples retain their identity. +# Command ids and context suffixes are immutable keys; translated text is display-only. +# Completeness measured against the English UI and live command/menu source sets; see tests. + +[meta] +name = "Suomi" +status = "draft — needs native review" + +[ui] +"% of all commits" = "% kaikista muutoksista" +"(no longer published)" = "(ei enää julkaistu)" +"({n} warnings: some entries were skipped)" = { one = "({n} varoitus: joitakin kohteita ohitettiin)", other = "({n} varoitusta: joitakin kohteita ohitettiin)" } +"+ Create variable" = "+ Luo muuttuja" +".fig (experimental)" = ".fig (kokeellinen)" +"1…9, 0" = "1…9, 0" +"@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: PR:t {prs}, muutokset {commits}, rivit +{added} / −{deleted} (Δ {delta}), binääriresurssit +{bin_added} / −{bin_deleted}, {first} – {last}" +"A .figcraft file with this name already exists. Choose its full name to confirm replacement." = "Tämänniminen .figcraft-tiedosto on jo olemassa. Vahvista korvaaminen valitsemalla sen koko nimi." +"A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "Tämän tiedoston kokoelma, joka sisältää nämä tilat ja muuttujat. Voit ohittaa niiden arvot tilakohtaisesti ja lisätä omia muuttujia." +"A layer on its own aligns to its frame: select more layers, or one in a frame" = "Yksittäinen taso tasataan kehykseensä. Valitse useampia tasoja tai yksi kehyksen sisällä oleva taso." +"A responsive product page: hero, features, figures, a quote and a footer" = "Mukautuva tuotesivu: pääosio, ominaisuudet, tunnusluvut, lainaus ja alatunniste" +"A spring plays until it settles: its mass, stiffness and damping set how long" = "Jousi liikkuu, kunnes se asettuu. Sen massa, jäykkyys ja vaimennus määräävät keston." +"A title, an agenda, a big figure, a chart and a closing slide" = "Otsikko, esityslista, suuri tunnusluku, kaavio ja päätösdia" +"A to Z" = "A:sta Z:aan" +"About" = "Tietoja" +"About FigCraft" = "Tietoja FigCraftista" +"About this server" = "Tietoja tästä palvelimesta" +"Action" = "Toiminto" +"Actions" = "Toiminnot" +"Actions for {name}" = "Kohteen {name} toiminnot" +"Actions…" = "Toiminnot…" +"Active" = "Aktiivinen" +"Actual size (100%)" = "Todellinen koko (100 %)" +"Add a category" = "Lisää luokka" +"Add a comment" = "Lisää kommentti" +"Add a description" = "Lisää kuvaus" +"Add a frame (F) or a shape (R) to get started" = "Aloita lisäämällä kehys (F) tai muoto (R)" +"Add a layer or component to tile first" = "Lisää ensin laatoitettava taso tai komponentti" +"Add a note for developers" = "Lisää huomautus kehittäjille" +"Add action" = "Lisää toiminto" +"Add actions for when no condition holds" = "Lisää toimintoja tilanteeseen, jossa mikään ehto ei täyty" +"Add an annotation for developers" = "Lisää huomautus kehittäjille" +"Add another condition" = "Lisää toinen ehto" +"Add auto layout" = "Lisää automaattinen asettelu" +"Add background behind overlay" = "Lisää tausta peittokuvan taakse" +"Add content to this slot" = "Lisää sisältöä tähän sisältöpaikkaan" +"Add dev resource" = "Lisää kehittäjäresurssi" +"Add effect" = "Lisää tehoste" +"Add export setting" = "Lisää vientiasetus" +"Add fill" = "Lisää täyttö" +"Add interaction" = "Lisää vuorovaikutus" +"Add layout grid" = "Lisää asetteluruudukko" +"Add library" = "Lisää kirjasto" +"Add library file…" = "Lisää kirjastotiedosto…" +"Add max height" = "Lisää enimmäiskorkeus" +"Add max width" = "Lisää enimmäisleveys" +"Add min height" = "Lisää vähimmäiskorkeus" +"Add min width" = "Lisää vähimmäisleveys" +"Add mode" = "Lisää tila" +"Add new page" = "Lisää uusi sivu" +"Add or update collections from a variables JSON" = "Lisää tai päivitä kokoelmia muuttujien JSON-tiedostosta" +"Add properties to make instances configurable" = "Lisää ominaisuuksia, jotta ilmentymiä voi mukauttaa" +"Add reaction" = "Lisää reaktio" +"Add starting point" = "Lisää aloituspiste" +"Add stroke" = "Lisää reunaviiva" +"Add variant" = "Lisää variantti" +"Add variant with a new value" = "Lisää variantti uudella arvolla" +"Added" = "Lisätty" +"Added the library \"{name}\"" = "Kirjasto ”{name}” lisätty" +"Adding to {name}" = "Lisätään kohteeseen {name}" +"Adjust" = "Säädä" +"Adjustment presets" = "Säädön esiasetukset" +"Advanced" = "Lisäasetukset" +"Advanced stroke" = "Reunaviivan lisäasetukset" +"After delay" = "Viiveen jälkeen" +"After delay {ms}ms" = "Viiveen {ms}ms jälkeen" +"Align" = "Tasaa" +"Align bottom" = "Tasaa alas" +"Align center" = "Keskitä" +"Align horizontal centers" = "Keskitä vaakasuunnassa" +"Align left" = "Tasaa vasemmalle" +"Align middle" = "Tasaa keskelle" +"Align right" = "Tasaa oikealle" +"Align text baselines" = "Tasaa tekstin peruslinjat" +"Align top" = "Tasaa ylös" +"Align vertical centers" = "Keskitä pystysuunnassa" +"Alignment" = "Tasaus" +"All" = "Kaikki" +"All categories" = "Kaikki luokat" +"All comments" = "Kaikki kommentit" +"All components" = "Kaikki komponentit" +"All fonts" = "Kaikki fontit" +"All supported properties" = "Kaikki tuetut ominaisuudet" +"All the fonts this file uses are installed" = "Kaikki tämän tiedoston käyttämät fontit on asennettu" +"All variables" = "Kaikki muuttujat" +"Alpha" = "Alfa" +"Alphabetical" = "Aakkosjärjestys" +"Alt+drag" = "Alt+drag" +"An analytics screen on a grid: figures, charts drawn from vectors and a table" = "Analytiikkanäkymä ruudukossa: tunnuslukuja, vektoreista piirrettyjä kaavioita ja taulukko" +"An invite link that opens FigCraft and joins" = "Kutsulinkki, joka avaa FigCraftin ja liittyy istuntoon" +"Anchor" = "Ankkuri" +"Android" = "Android" +"Angular" = "Kulmaliukuväri" +"Animation" = "Animaatio" +"Annotations" = "Huomautukset" +"Annotations shown on the canvas" = "Piirtoalueella näytettävät huomautukset" +"Another server, or one that needs its key. The usual server is set in Preferences." = "Toinen palvelin tai avainta tarvitseva palvelin. Tavallinen palvelin asetetaan asetuksissa." +"Any status" = "Kaikki tilat" +"Anyone on your network with this code can edit the file, while this app stays open." = "Kuka tahansa lähiverkossasi voi muokata tiedostoa tällä koodilla niin kauan kuin sovellus on auki." +"Anyone with the edit link can change the file; the view link only lets people look. The links carry their secrets: share them like passwords. `figcraft-server links` makes more and revokes them." = "Kuka tahansa muokkauslinkin saanut voi muuttaa tiedostoa. Katselulinkillä voi vain katsella. Linkit sisältävät salaiset tunnisteet: jaa niitä kuin salasanoja. `figcraft-server links` luo lisää linkkejä ja peruu niitä." +"Anyone with this code can edit the file." = "Kuka tahansa tämän koodin saanut voi muokata tiedostoa." +"Anyone with this code can look at the file, not change it." = "Kuka tahansa tämän koodin saanut voi katsella tiedostoa, mutta ei muuttaa sitä." +"Appearance" = "Ulkoasu" +"Apply a component property" = "Käytä komponentin ominaisuutta" +"Apply variable" = "Käytä muuttujaa" +"Apply variable to text" = "Käytä muuttujaa tekstiin" +"Apply variable to visibility" = "Käytä muuttujaa näkyvyyteen" +"Apps" = "Sovellukset" +"Apr" = "huhtik." +"Arc" = "Kaari" +"Arc ratio" = "Kaaren suhde" +"Arc start" = "Kaaren alku" +"Arc sweep" = "Kaaren pyyhkäisy" +"Archive" = "Arkistoi" +"Archived" = "Arkistoitu" +"Arrange" = "Järjestä" +"Arrow" = "Nuoli" +"As typed" = "Kirjoitetussa muodossa" +"Asking the server…" = "Kysytään palvelimelta…" +"Assets" = "Resurssit" +"Aug" = "elok." +"Auto" = "Automaattinen" +"Auto (space around)" = "Automaattinen (tila ympärillä)" +"Auto (space between)" = "Automaattinen (tila välissä)" +"Auto (space evenly)" = "Automaattinen (tasavälein)" +"Auto ({mode})" = "Automaattinen ({mode})" +"Auto columns" = "Automaattiset sarakkeet" +"Auto gap" = "Automaattinen väli" +"Auto height" = "Automaattinen korkeus" +"Auto layout" = "Automaattinen asettelu" +"Auto rows" = "Automaattiset rivit" +"Auto width" = "Automaattinen leveys" +"Back" = "Takaisin" +"Back to Ready for dev" = "Palauta kehitysvalmiiksi" +"Back to Ready for dev: the next change compares with how it is now" = "Palauta kehitysvalmiiksi: seuraavaa muutosta verrataan nykyiseen tilaan" +"Back to assets" = "Takaisin resursseihin" +"Back to current" = "Takaisin nykyiseen" +"Back to styles" = "Takaisin tyyleihin" +"Back to the FigCraft server" = "Takaisin FigCraft-palvelimelle" +"Back to the canvas from a focused region" = "Palaa kohdistetulta alueelta piirtoalueelle" +"Back to the current version" = "Takaisin nykyiseen versioon" +"Back to the font's value" = "Palauta fontin arvo" +"Back to the values the font style implies" = "Palauta fonttityylin mukaiset arvot" +"Background" = "Tausta" +"Background blur" = "Taustan sumennus" +"Basics" = "Perusteet" +"Before" = "Ennen" +"Bend" = "Taivutus" +"Bevel" = "Viiste" +"Binary assets added" = "Lisätyt binääriresurssit" +"Binary assets removed" = "Poistetut binääriresurssit" +"Black & white" = "Mustavalkoinen" +"Bleed" = "Leikkuuvara" +"Bleed marks" = "Leikkuuvaramerkit" +"Bleed marks need a bleed" = "Leikkuuvaramerkit edellyttävät leikkuuvaraa" +"Blend" = "Sekoitus" +"Blend mode" = "Sekoitustila" +"Blue" = "Sininen" +"Blur" = "Sumennus" +"Boolean" = "Totuusarvo" +"Boolean groups" = "Boolen ryhmät" +"Both directions" = "Molemmat suunnat" +"Bottom" = "Alhaalta" +"Bottom left radius" = "Vasemman alakulman säde" +"Bottom right radius" = "Oikean alakulman säde" +"Bound to {name}. Click to detach the variable." = "Sidottu muuttujaan {name}. Irrota muuttuja napsauttamalla." +"Branch" = "Haara" +"Branch archived" = "Haara arkistoitu" +"Branch merged" = "Haara yhdistetty" +"Branch name" = "Haaran nimi" +"Branch of {file}" = "Tiedoston {file} haara" +"Branch of {file} · merged or archived" = "Tiedoston {file} haara · yhdistetty tai arkistoitu" +"Branch updated from main" = "Haara päivitetty päähaarasta" +"Branch {name}" = "Haara {name}" +"Branches" = "Haarat" +"Bring forward" = "Tuo eteenpäin" +"Bring the main file's changes into the branch" = "Tuo päätiedoston muutokset haaraan" +"Bring to front" = "Tuo etummaiseksi" +"Bristle" = "Harjas" +"Browse" = "Selaa" +"Brush" = "Sivellin" +"Brush rotation jitter" = "Siveltimen kierron vaihtelu" +"Brush scatter" = "Siveltimen hajonta" +"Brush size jitter" = "Siveltimen koon vaihtelu" +"Brush spacing" = "Siveltimen välistys" +"Bulleted list" = "Luettelomerkitty luettelo" +"C or Shift+C" = "C or Shift+C" +"C or ⇧C" = "C or ⇧C" +"CMYK colour" = "CMYK-väri" +"CSS custom properties…" = "CSS:n mukautetut ominaisuudet…" +"Can edit" = "Voi muokata" +"Can view" = "Voi katsella" +"Can't read this library: {e}" = "Kirjastoa ei voi lukea: {e}" +"Cancel" = "Peruuta" +"Canvas" = "Piirtoalue" +"Canvas stacking" = "Piirtoalueen pinoamisjärjestys" +"Cap height to baseline" = "Versaalikorkeudesta peruslinjaan" +"Case" = "Kirjainkoko" +"Case-sensitive forms" = "Kirjainkokoon mukautuvat muodot" +"Categories" = "Luokat" +"Center" = "Keskelle" +"Change alias" = "Vaihda alias" +"Change resolved" = "Muutos ratkaistu" +"Change style" = "Vaihda tyyli" +"Change to" = "Vaihda kohteeksi" +"Change to needs a layer inside a variant" = "Vaihto edellyttää variantin sisällä olevaa tasoa" +"Changed" = "Muutettu" +"Changed on both sides: keep one" = "Muutettu molemmilla puolilla: säilytä toinen" +"Changes on main" = "Päähaaran muutokset" +"Changes on the branch" = "Haaran muutokset" +"Changes unknown" = "Muutokset tuntemattomia" +"Checking the fonts installed on this computer…" = "Tarkistetaan tämän tietokoneen asennettuja fontteja…" +"Checking the server…" = "Tarkistetaan palvelinta…" +"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Valitse yllä React, Leptos, SwiftUI tai Compose yhdistääksesi tämän komponentin sen koodiin." +"Choose another profile to change it." = "Vaihda valitsemalla toinen profiili." +"Choose collection" = "Valitse kokoelma" +"Choose image" = "Valitse kuva" +"Choose mode" = "Valitse tila" +"Choose variable" = "Valitse muuttuja" +"Circle" = "Ympyrä" +"Clear from this list" = "Poista tästä luettelosta" +"Clear recent" = "Tyhjennä viimeisimmät" +"Click + to replace mixed content" = "Korvaa vaihteleva sisältö napsauttamalla +" +"Click anywhere on the canvas to leave one." = "Lisää huomautus napsauttamalla mitä tahansa piirtoalueen kohtaa." +"Click or drag to merge · {alt} removes" = "Yhdistä napsauttamalla tai vetämällä · {alt} poistaa" +"Click or drag to remove · {alt} merges" = "Poista napsauttamalla tai vetämällä · {alt} yhdistää" +"Click to record a key" = "Tallenna näppäin napsauttamalla" +"Clip content" = "Leikkaa sisältö rajoihin" +"Clip to shape" = "Leikkaa muotoon" +"Close" = "Sulje" +"Close overlay" = "Sulje peittokuva" +"Close preview" = "Sulje esikatselu" +"Close tab" = "Sulje välilehti" +"Close version history" = "Sulje versiohistoria" +"Close when clicking outside" = "Sulje napsautettaessa ulkopuolelle" +"Close without saving" = "Sulje tallentamatta" +"Code" = "Koodi" +"Code or invite link" = "Koodi tai kutsulinkki" +"Code syntax" = "Koodin syntaksi" +"Collapse" = "Tiivistä muunnokset" +"Collapse panel" = "Supista paneeli" +"Collection" = "Kokoelma" +"Collections" = "Kokoelmat" +"Color" = "Väri" +"Color burn" = "Värivarjostus" +"Color dodge" = "Värilisävalotus" +"Color format" = "Värimuoto" +"Color profile" = "Väriprofiili" +"Color profile: {name}" = "Väriprofiili: {name}" +"Color styles" = "Värityylit" +"Color values in this file are in this color space." = "Tämän tiedoston väriarvot ovat tässä väriavaruudessa." +"Color variables" = "Värimuuttujat" +"Colour" = "Väri" +"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Väri- ja tekstityylejä, vaalean ja tumman tilan muuttujia, painikkeita, syötekenttiä ja kortteja" +"Column" = "Sarake" +"Column gap" = "Sarakkeiden väli" +"Column sizes" = "Sarakkeiden koot" +"Column span" = "Sarakeulottuvuus" +"Columns" = "Sarakkeet" +"Columns {count}" = "Sarakkeita: {count}" +"Comment" = "Kommentti" +"Comments" = "Kommentit" +"Commits" = "Muutokset" +"Company" = "Yritys" +"Compare changes" = "Vertaa muutoksia" +"Compatibility notes" = "Yhteensopivuushuomautukset" +"Completed" = "Valmis" +"Component" = "Komponentti" +"Component name" = "Komponentin nimi" +"Component property" = "Komponentin ominaisuus" +"Component set" = "Komponenttijoukko" +"Component sets" = "Komponenttijoukot" +"Components" = "Komponentit" +"Conditional" = "Ehdollinen" +"Confetti" = "Konfetti" +"Conflicts" = "Ristiriidat" +"Connect to code" = "Yhdistä koodiin" +"Connected" = "Yhdistetty" +"Connected to code: {from}" = "Yhdistetty koodiin: {from}" +"Connection lost, reconnecting (attempt {attempt})…" = "Yhteys katkesi, yhdistetään uudelleen (yritys {attempt})…" +"Constrain proportions" = "Säilytä mittasuhteet" +"Constraints" = "Rajoitteet" +"Content" = "Sisältö" +"Contents hidden" = "Sisältö kätketty" +"Contents only" = "Vain sisältö" +"Contextual alternates" = "Kontekstuaaliset vaihtoehdot" +"Contrast" = "Kontrasti" +"Contributors" = "Tekijät" +"Contributors: {people} · Commits: {commits}" = "Tekijät: {people} · Muutokset: {commits}" +"Controller" = "Ohjain" +"Convert to frame" = "Muunna kehykseksi" +"Convert to section" = "Muunna osioksi" +"Cool" = "Viileä" +"Copied as CSS" = "Kopioitu CSS-muodossa" +"Copied as PNG" = "Kopioitu PNG-muodossa" +"Copied as SVG" = "Kopioitu SVG-muodossa" +"Copied as text" = "Kopioitu tekstinä" +"Copied {value}" = "Kopioitu: {value}" +"Copy" = "Kopioi" +"Copy as" = "Kopioi muodossa" +"Copy as CSS" = "Kopioi CSS-muodossa" +"Copy as PNG" = "Kopioi PNG-muodossa" +"Copy as SVG" = "Kopioi SVG-muodossa" +"Copy code" = "Kopioi koodi" +"Copy link" = "Kopioi linkki" +"Copy the {label}" = "Kopioi: {label}" +"Copy {value}" = "Kopioi: {value}" +"Corner radius" = "Kulman säde" +"Corner smoothing" = "Kulman pehmennys" +"Couldn't download the variables" = "Muuttujia ei voitu ladata" +"Couldn't reach {server}: showing what this computer remembers." = "Palvelimeen {server} ei saatu yhteyttä. Näytetään tämän tietokoneen muistamat tiedot." +"Couldn't save automatically: {error}" = "Automaattinen tallennus epäonnistui: {error}" +"Count" = "Määrä" +"Create" = "Luo" +"Create a variable collection first" = "Luo ensin muuttujakokoelma" +"Create a variable first" = "Luo ensin muuttuja" +"Create alias" = "Luo alias" +"Create branch" = "Luo haara" +"Create collection" = "Luo kokoelma" +"Create component" = "Luo komponentti" +"Create component property" = "Luo komponentin ominaisuus" +"Create components, styles or variables to publish them as a library." = "Luo komponentteja, tyylejä tai muuttujia, jotta voit julkaista ne kirjastona." +"Create property…" = "Luo ominaisuus…" +"Create style" = "Luo tyyli" +"Create style from the selected text" = "Luo tyyli valitusta tekstistä" +"Created" = "Luotu" +"Crop" = "Rajaa" +"Crop marks" = "Leikkausmerkit" +"Ctrl+arrows" = "Ctrl+arrows" +"Ctrl+click" = "Ctrl+click" +"Ctrl+scroll" = "Ctrl+scroll" +"Current name" = "Nykyinen nimi" +"Current version" = "Nykyinen versio" +"Cursor chat" = "Kohdistinkeskustelu" +"Custom" = "Mukautettu" +"Custom size" = "Mukautettu koko" +"Cut" = "Leikkaa" +"Dark" = "Tumma" +"Darken" = "Tummenna" +"Dash" = "Katkoviiva" +"Dashboard" = "Koontinäkymä" +"Date created" = "Luontipäivä" +"Dec" = "jouluk." +"Decoration" = "Koristelu" +"Decoration style" = "Koristelun tyyli" +"Deep select" = "Valitse sisäkkäinen kohde" +"Default" = "Oletus" +"Default content" = "Oletussisältö" +"Default value (the top-left variant)" = "Oletusarvo (vasemman yläkulman variantti)" +"Delay" = "Viive" +"Delete" = "Poista" +"Delete category (its annotations stay)" = "Poista luokka (sen huomautukset säilyvät)" +"Delete collection" = "Poista kokoelma" +"Delete group" = "Poista ryhmä" +"Delete mode" = "Poista tila" +"Delete page" = "Poista sivu" +"Delete preset" = "Poista esiasetus" +"Delete property" = "Poista ominaisuus" +"Delete the style (layers keep their values)" = "Poista tyyli (tasojen arvot säilyvät)" +"Delete thread" = "Poista keskustelu" +"Delete variable" = "Poista muuttuja" +"Delete “{name}”? This can't be undone." = "Poistetaanko ”{name}”? Tätä ei voi kumota." +"Density" = "Tiheys" +"Depth" = "Syvyys" +"Describe how and when to use this component" = "Kuvaile, miten ja milloin tätä komponenttia käytetään" +"Describe what changed" = "Kuvaile muutokset" +"Description" = "Kuvaus" +"Description (optional)" = "Kuvaus (valinnainen)" +"Design" = "Suunnittelu" +"Design system starter" = "Muotoilujärjestelmän aloituspohja" +"Desktop" = "Työpöytä" +"Destination" = "Kohde" +"Detach alias" = "Irrota alias" +"Detach instance" = "Irrota ilmentymä" +"Detach property" = "Irrota ominaisuus" +"Detach style" = "Irrota tyyli" +"Detach styles" = "Irrota tyylit" +"Detach variable" = "Irrota muuttuja" +"Details" = "Tiedot" +"Dev Mode" = "Kehittäjätila" +"Dev resources" = "Kehittäjäresurssit" +"Development" = "Kehitys" +"Device" = "Laite" +"Device at 100%" = "Laite koossa 100 %" +"Diamond" = "Vinoneliö" +"Difference" = "Erotus" +"Dimensions" = "Mitat" +"Direction" = "Suunta" +"Disconnect from code" = "Katkaise yhteys koodiin" +"Dispersion" = "Hajonta" +"Display name" = "Näyttönimi" +"Dissolve" = "Häivytys" +"Distribute and tidy" = "Jaa ja järjestä" +"Distribute horizontal spacing" = "Jaa vaakavälit tasan" +"Distribute vertical spacing" = "Jaa pystyvälit tasan" +"Divider" = "Erotin" +"Don't truncate" = "Älä lyhennä" +"Done" = "Valmis" +"Done editing" = "Lopeta muokkaus" +"Dots" = "Pisteet" +"Dotted" = "Pisteviiva" +"Down" = "Alas" +"Drafts" = "Luonnokset" +"Drag the stroke to add a width point" = "Lisää leveyspiste vetämällä reunaviivaa" +"Draw" = "Piirrä" +"Draw a frame" = "Piirrä kehys" +"Drawing tools" = "Piirtotyökalut" +"Drop a .figcraft, .fig, .json, .svg or image file." = "Pudota .figcraft-, .fig-, .json-, .svg- tai kuvatiedosto." +"Drop shadow" = "Heittovarjo" +"Drop to import" = "Tuo pudottamalla" +"Drop to open" = "Avaa pudottamalla" +"Dual-licensed MIT OR Apache-2.0. Made by the ArtCraft team and community." = "Kaksoislisensoitu: MIT OR Apache-2.0. ArtCraft-tiimin ja yhteisön tekemä." +"Duo" = "Kaksivärinen" +"Duplicate" = "Monista" +"Duplicate by dragging" = "Monista vetämällä" +"Duplicate mode" = "Monista tila" +"Duplicate page" = "Monista sivu" +"Edit" = "Muokkaa" +"Edit as an expression" = "Muokkaa lausekkeena" +"Edit colour" = "Muokkaa väriä" +"Edit comment" = "Muokkaa kommenttia" +"Edit dev resource" = "Muokkaa kehittäjäresurssia" +"Edit link" = "Muokkaa linkkiä" +"Edit link copied" = "Muokkauslinkki kopioitu" +"Edit style" = "Muokkaa tyyliä" +"Edit variable" = "Muokkaa muuttujaa" +"Edit with the builder" = "Muokkaa rakentimella" +"Edited {when}" = "Muokattu {when}" +"Effect settings" = "Tehosteen asetukset" +"Effect styles" = "Tehostetyylit" +"Effects" = "Tehosteet" +"Ellipse" = "Ellipsi" +"Ellipses" = "Ellipsit" +"Else" = "Muuten" +"Else if" = "Muuten jos" +"Embedded profile" = "Upotettu profiili" +"Empty" = "Tyhjä" +"End" = "Loppu" +"End point" = "Loppupiste" +"Ends the session; everyone keeps their copy" = "Lopettaa istunnon. Kaikki säilyttävät oman kopionsa." +"Enter" = "Enter" +"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Anna figcraft-server-palvelimen osoite ja näytä sen tarjoamat liitännäiset painamalla Selaa." +"Enter to save · Esc to cancel" = "Tallenna Enterillä · peruuta Escillä" +"Eraser" = "Pyyhekumi" +"Esc" = "Esc" +"Essential" = "Olennainen" +"Every conflict has a choice." = "Jokaiselle ristiriidalle on valittu ratkaisu." +"Every top-level frame already starts a flow" = "Jokainen ylimmän tason kehys aloittaa jo kulun" +"Exact opacity (4 then 5: 45%)" = "Tarkka peittävyys (4 ja sitten 5: 45 %)" +"Exclude" = "Jätä pois" +"Excluded" = "Jätetty pois" +"Excluded from layout" = "Jätetty pois asettelusta" +"Excludes" = "Poissulkee" +"Exclusion" = "Poissulku" +"Exit focus view (Esc)" = "Poistu kohdistusnäkymästä (Esc)" +"Expand" = "Laajenna" +"Export" = "Vie" +"Export all" = "Vie kaikki" +"Export frames and layers as PNG, JPG, SVG or PDF" = "Vie kehyksiä ja tasoja PNG-, JPG-, SVG- tai PDF-muodossa" +"Export page as PNG" = "Vie sivu PNG-muodossa" +"Export settings" = "Vientiasetukset" +"Export variables" = "Vie muuttujat" +"Export {name}" = "Vie {name}" +"Export {n} layers" = { one = "Vie {n} taso", other = "Vie {n} tasoa" } +"Exported {file}" = "Viety: {file}" +"Exported {name}" = "Viety: {name}" +"Exported {name}; {n} kinds of content were left out" = { one = "Viety {name}; {n} sisältötyyppi jätettiin pois", other = "Viety {name}; {n} sisältötyyppiä jätettiin pois" } +"Exported {n} files" = { one = "Viety {n} tiedosto", other = "Viety {n} tiedostoa" } +"Export…" = "Vie…" +"Expose properties to instances" = "Tuo ominaisuudet ilmentymien käyttöön" +"Exposure" = "Valotus" +"Expression" = "Lauseke" +"Extend collection" = "Laajenna kokoelmaa" +"Extends \"{name}\": overridden values are marked" = "Laajentaa kokoelmaa ”{name}”: ohitetut arvot on merkitty" +"Eye" = "Silmä" +"F" = "F" +"Fade" = "Häivytys" +"False" = "Epätosi" +"Feb" = "helmik." +"Fetching from the server…" = "Haetaan palvelimelta…" +"FigCraft couldn't open its window: {error}" = "FigCraft ei voinut avata ikkunaansa: {error}" +"FigCraft doesn't support every feature of these files yet. The notes list what was left out or changed in the imported copy; the original file is unchanged." = "FigCraft ei vielä tue näiden tiedostojen kaikkia ominaisuuksia. Huomautukset kertovat, mitä tuodusta kopiosta jätettiin pois tai muutettiin. Alkuperäinen tiedosto säilyy ennallaan." +"File" = "Tiedosto" +"File name" = "Tiedostonimi" +"File not found" = "Tiedostoa ei löydy" +"File published" = "Tiedosto julkaistu" +"Files aren't end-to-end encrypted: the server's operator can read them." = "Tiedostoja ei salata päästä päähän. Palvelimen ylläpitäjä voi lukea ne." +"Files nobody opens for {n} days are deleted." = { one = "Tiedostot, joita kukaan ei avaa {n} päivään, poistetaan.", other = "Tiedostot, joita kukaan ei avaa {n} päivään, poistetaan." } +"Fill" = "Täyttö" +"Fill (fr)" = "Täytä (fr)" +"Fill container" = "Täytä säilö" +"Fill window" = "Täytä ikkuna" +"Fills" = "Täytöt" +"Fill|sizing" = "Täytä" +"Filter and sort" = "Suodata ja lajittele" +"Find" = "Etsi" +"Find and replace" = "Etsi ja korvaa" +"Find layers" = "Etsi tasoja" +"First" = "Ensimmäinen" +"First commit" = "Ensimmäinen muutos" +"First on top" = "Ensimmäinen päällimmäisenä" +"Fit" = "Sovita" +"Fit device to window" = "Sovita laite ikkunaan" +"Fit to contents" = "Sovita sisältöön" +"Fit to window" = "Sovita ikkunaan" +"Fit width" = "Sovita leveyteen" +"Fixed" = "Kiinteä" +"Fixed gap" = "Kiinteä väli" +"Fixed size" = "Kiinteä koko" +"Flatten" = "Yhdistä vektorimuodoksi" +"Flip" = "Peilaa" +"Flip horizontal" = "Peilaa vaakasuunnassa" +"Flip text to the other side" = "Siirrä teksti toiselle puolelle" +"Flip vertical" = "Peilaa pystysuunnassa" +"Flip width profile" = "Peilaa leveysprofiili" +"Flow starting point" = "Kulun aloituspiste" +"Flow starting points" = "Kulkujen aloituspisteet" +"Flows" = "Kulut" +"Focus view" = "Kohdistusnäkymä" +"Fold autosaves" = "Tiivistä automaattiset tallennukset" +"Folder: click to collapse" = "Kansio: supista napsauttamalla" +"Folder: click to expand" = "Kansio: laajenna napsauttamalla" +"Follow" = "Seuraa" +"Follow {name}" = "Seuraa käyttäjää {name}" +"Following" = "Seurataan" +"Following {name} · {key} to stop" = "Seurataan käyttäjää {name} · lopeta painamalla {key}" +"Font" = "Fontti" +"Font not installed" = "Fonttia ei ole asennettu" +"Font size" = "Fonttikoko" +"Font style" = "Fonttityyli" +"Forget on this computer" = "Unohda tällä tietokoneella" +"Four screens of a trail app with components and a prototype flow" = "Retkeilysovelluksen neljä näkymää, komponentit ja prototyypin kulku" +"Fractions" = "Murtoluvut" +"Frame" = "Kehys" +"Frame selection" = "Kehystä valinta" +"Frame tools" = "Kehystyökalut" +"Frames" = "Kehykset" +"From the library \"{name}\": read-only" = "Kirjastosta ”{name}”: vain luku" +"From {name}" = "Lähteestä {name}" +"From {server}" = "Palvelimelta {server}" +"Frost" = "Huurre" +"Gap" = "Väli" +"Gap between columns" = "Sarakkeiden väli" +"Gap between items" = "Kohteiden väli" +"Gap between rows" = "Rivien väli" +"Getting started" = "Aloittaminen" +"Give the address of a server open to the public, or a server's address with its key (`figcraft-server key` prints it)." = "Anna julkisen palvelimen osoite tai palvelimen osoite avaimineen (`figcraft-server key` tulostaa avaimen)." +"Glass" = "Lasi" +"Go to main component" = "Siirry pääkomponenttiin" +"Go to {name}" = "Siirry kohteeseen {name}" +"Grab bag" = "Sekalaiset" +"Gradient" = "Liukuväri" +"Gray" = "Harmaa" +"Green" = "Vihreä" +"Greyscale components and two example screens to sketch a flow with" = "Harmaasävykomponentit ja kaksi esimerkkinäkymää kulun luonnosteluun" +"Grid" = "Ruudukko" +"Grid ({size})" = "Ruudukko ({size})" +"Grid styles" = "Ruudukkotyylit" +"Grid {size}" = "Ruudukko {size}" +"Group" = "Ryhmä" +"Group selection" = "Ryhmittele valinta" +"Groups" = "Ryhmät" +"Gutter" = "Palstaväli" +"Hand tool" = "Käsityökalu" +"Hanging list" = "Riippuva luettelo" +"Hanging punctuation" = "Välimerkkien ulonnus" +"Hard light" = "Kova valo" +"Height" = "Korkeus" +"Help" = "Ohje" +"Help and resources" = "Ohje ja resurssit" +"Help page" = "Ohjesivu" +"Here: {names}" = "Paikalla: {names}" +"Hidden" = "Kätketty" +"Hide contents" = "Kätke sisältö" +"Hide from publishing" = "Jätä julkaisematta" +"Hide section contents" = "Kätke osion sisältö" +"Hide these tips (Getting started in the Help menu shows them again)" = "Kätke nämä vinkit (Ohje-valikon Aloittaminen näyttää ne uudelleen)" +"Hide when publishing" = "Kätke julkaistaessa" +"Highlight layers on hover" = "Korosta tasot osoitettaessa" +"Highlights" = "Huippuvalot" +"Hold Alt" = "Pidä Alt painettuna" +"Hold Alt+Z" = "Pidä Alt+Z painettuna" +"Hold Ctrl" = "Pidä Ctrl painettuna" +"Hold Z" = "Pidä Z painettuna" +"Hold ⌃ or ⌘" = "Pidä ⌃ tai ⌘ painettuna" +"Hold ⌥" = "Pidä ⌥ painettuna" +"Hold ⌥Z" = "Pidä ⌥Z painettuna" +"Home" = "Aloitus" +"Horizontal" = "Vaakasuunta" +"Horizontal align" = "Vaakatasaus" +"Horizontal hex" = "Vaakasuuntainen kuusikulmio" +"Horizontal padding" = "Vaakasuuntainen sisämarginaali" +"Horizontal spacing" = "Vaakavälistys" +"Hosted by {operator}" = "Ylläpitäjä: {operator}" +"Hover a layer to see its properties" = "Näytä tason ominaisuudet viemällä osoitin sen päälle" +"Hover other layers to measure the distance" = "Mittaa etäisyys viemällä osoitin muiden tasojen päälle" +"Hue" = "Sävy" +"Hug" = "Sovita sisältöön" +"Hug contents" = "Sovita sisältöön" +"Icon components on a 24 px grid with keylines and a size preview" = "Kuvakekomponentteja 24 px:n ruudukossa, apulinjat ja koon esikatselu" +"Icon set starter" = "Kuvakejoukon aloituspohja" +"If" = "Jos" +"If it keeps happening, please report it at {link} with this message." = "Jos ongelma jatkuu, ilmoita siitä osoitteessa {link} ja liitä tämä viesti mukaan." +"If {condition}" = "Jos {condition}" +"Ignore auto layout (absolute position)" = "Ohita automaattinen asettelu (absoluuttinen sijainti)" +"Image" = "Kuva" +"Images" = "Kuvat" +"Import" = "Tuo" +"Import path" = "Tuontipolku" +"Import plugin from manifest" = "Tuo liitännäinen manifestista" +"Import plugin from manifest…" = "Tuo liitännäinen manifestista…" +"Import report" = "Tuontiraportti" +"Import variables" = "Tuo muuttujat" +"Imported document" = "Tuotu asiakirja" +"Imported {name}" = "Tuotu: {name}" +"Imported {n} new variables, updated {updated}" = { one = "Tuotu {n} uusi muuttuja, päivitetty {updated}", other = "Tuotu {n} uutta muuttujaa, päivitetty {updated}" } +"Import…" = "Tuo…" +"In an instance, this comes from the main component" = "Ilmentymässä tämä arvo tulee pääkomponentista" +"In the browser you can join sessions the desktop app hosts and files on a FigCraft server. A page opened over https reaches other computers only through wss:// links (a server behind TLS)." = "Selaimessa voit liittyä työpöytäsovelluksen isännöimiin istuntoihin ja FigCraft-palvelimen tiedostoihin. https-sivu tavoittaa muut tietokoneet vain wss://-linkeillä (TLS:n takana oleva palvelin)." +"Include bounding box" = "Sisällytä rajauslaatikko" +"Include id attribute" = "Sisällytä id-määrite" +"Included" = "Sisällytetty" +"Included in layout" = "Sisällytetty asetteluun" +"Individual corners" = "Kulmat erikseen" +"Individual strokes" = "Reunaviivat erikseen" +"Ink" = "Muste" +"Inner shadow" = "Sisävarjo" +"Inside" = "Sisäpuolella" +"Inspect" = "Tarkastele" +"Inspect the layer under the pointer: size, colours, typography" = "Tarkastele osoittimen alla olevaa tasoa: koko, värit ja typografia" +"Install" = "Asenna" +"Install plugin" = "Asenna liitännäinen" +"Installed" = "Asennettu" +"Installed {name}" = "Asennettu: {name}" +"Installing…" = "Asennetaan…" +"Instance" = "Ilmentymä" +"Instance properties" = "Ilmentymän ominaisuudet" +"Instance swap" = "Ilmentymän vaihto" +"Instance swap (no other component)" = "Ilmentymän vaihto (ei muuta komponenttia)" +"Instances" = "Ilmentymät" +"Instant" = "Välitön" +"Intensity" = "Vahvuus" +"Interaction" = "Vuorovaikutus" +"Interactions" = "Vuorovaikutukset" +"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Käyttöliittymien suunnittelu ja prototyypit. Ilmainen avoimen lähdekoodin suunnittelutyökalu, joka on toteutettu riippumattomasti alusta alkaen Rustilla. Osa ArtCraft-sovellusperhettä." +"Intersect" = "Leikkaus" +"Intersects" = "Leikkaa" +"Invalid color" = "Virheellinen väri" +"Invite link" = "Kutsulinkki" +"Invite link copied" = "Kutsulinkki kopioitu" +"It asks permission to:" = "Se pyytää oikeutta:" +"Jan" = "tammik." +"Join" = "Liity" +"Join code copied" = "Liittymiskoodi kopioitu" +"Join code or invite link" = "Liittymiskoodi tai kutsulinkki" +"Join live session" = "Liity reaaliaikaiseen istuntoon" +"Join us on Discord" = "Liity yhteisöömme Discordissa" +"Join with a code" = "Liity koodilla" +"Joined" = "Liitytty" +"Joined {file}" = "Liitytty tiedostoon {file}" +"Joining…" = "Liitytään…" +"Join|stroke" = "Liitos" +"Jul" = "heinäk." +"Jun" = "kesäk." +"Just now" = "Juuri nyt" +"Justify" = "Tasaa molemmat reunat" +"Keep appearance" = "Säilytä ulkoasu" +"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "Säilytä ulkoasu muuntaa kaikki väriarvot profiiliin {profile}, jotta värit näyttävät samoilta. Säilytä arvot jättää lukuarvot ennalleen, jolloin värit muuttuvat." +"Keep tool selected after use" = "Pidä työkalu valittuna käytön jälkeen" +"Keep values" = "Säilytä arvot" +"Key/Gamepad" = "Näppäin/peliohjain" +"Key/Gamepad {key}" = "Näppäin/peliohjain {key}" +"Keyboard shortcuts" = "Pikanäppäimet" +"Kits" = "Paketit" +"Landing page" = "Laskeutumissivu" +"Language" = "Kieli" +"Lasso" = "Lasso" +"Last" = "Viimeinen" +"Last change: {note}" = "Viimeisin muutos: {note}" +"Last commit" = "Viimeisin muutos" +"Last modified" = "Viimeksi muokattu" +"Last on top" = "Viimeinen päällimmäisenä" +"Layer blur" = "Tason sumennus" +"Layer name" = "Tason nimi" +"Layers" = "Tasot" +"Layers in an instance take their position from the main component" = "Ilmentymän tasot saavat sijaintinsa pääkomponentista" +"Layout" = "Asettelu" +"Layout grid" = "Asetteluruudukko" +"Layout grid settings" = "Asetteluruudukon asetukset" +"Layout grids" = "Asetteluruudukot" +"Leave session" = "Poistu istunnosta" +"Leaves" = "Lehdet" +"Left" = "Vasen" +"Left & right" = "Vasen ja oikea" +"Left side" = "Vasen puoli" +"Left side width" = "Vasemman puolen leveys" +"Letter case" = "Kirjainkoko" +"Letter spacing" = "Merkkiväli" +"Libraries" = "Kirjastot" +"Libraries in this file" = "Tämän tiedoston kirjastot" +"Library updates available" = "Kirjastopäivityksiä saatavilla" +"Library: click to collapse" = "Kirjasto: supista napsauttamalla" +"Library: click to expand" = "Kirjasto: laajenna napsauttamalla" +"Library: its variables are read-only here; edit them in the library file and publish it" = "Kirjasto: sen muuttujat ovat tässä vain luettavissa. Muokkaa niitä kirjastotiedostossa ja julkaise se." +"Light" = "Vaalea" +"Lighten" = "Vaalenna" +"Light|glass" = "Valo" +"Line" = "Viiva" +"Line arrow" = "Viivanuoli" +"Line delta" = "Rivimuutos" +"Line height" = "Rivikorkeus" +"Linear" = "Lineaarinen" +"Lines" = "Viivat" +"Lines +/−" = "Rivit +/−" +"Lines added" = "Lisätyt rivit" +"Lines deleted" = "Poistetut rivit" +"Link" = "Linkki" +"Link copied" = "Linkki kopioitu" +"Link docs, code or tickets for developers." = "Linkitä dokumentaatiota, koodia tai tehtäviä kehittäjille." +"Link frames on the Prototype tab, then present the flow" = "Linkitä kehykset Prototyyppi-välilehdellä ja esitä sitten kulku" +"Link this component to its code component: instances then show your real snippet with their values." = "Yhdistä tämä komponentti sen koodikomponenttiin. Ilmentymät näyttävät sitten todellisen koodikatkelmasi omilla arvoillaan." +"List spacing" = "Luettelon välistys" +"List style" = "Luettelotyyli" +"Live session" = "Reaaliaikainen istunto" +"Live · {people}" = "Reaaliaikainen · {people}" +"Local components" = "Paikalliset komponentit" +"Local network" = "Paikallisverkko" +"Local styles" = "Paikalliset tyylit" +"Local variables" = "Paikalliset muuttujat" +"Lock/Unlock" = "Lukitse/avaa lukitus" +"Locked" = "Lukittu" +"Lowercase" = "Pienet kirjaimet" +"Luminance" = "Luminanssi" +"Luminosity" = "Valoisuus" +"Main" = "Päähaara" +"Main component deleted" = "Pääkomponentti poistettu" +"Main menu" = "Päävalikko" +"Make a component" = "Luo komponentti" +"Make slot" = "Luo sisältöpaikka" +"Mar" = "maalisk." +"Margin" = "Marginaali" +"Mark all as read" = "Merkitse kaikki luetuiksi" +"Mark as resolved" = "Merkitse ratkaistuksi" +"Mark as unread" = "Merkitse lukemattomaksi" +"Mark as unresolved" = "Merkitse ratkaisemattomaksi" +"Mask" = "Maski" +"Mass · Stiffness · Damping" = "Massa · jäykkyys · vaimennus" +"Match (empty: the whole name)" = "Etsittävä (tyhjä: koko nimi)" +"Max H" = "Enimmäiskorkeus" +"Max W" = "Enimmäisleveys" +"Max lines" = "Rivien enimmäismäärä" +"Maximize" = "Suurenna" +"May" = "toukok." +"Measure to the selection" = "Mittaa valintaan" +"Merge into main" = "Yhdistä päähaaraan" +"Merge regions" = "Yhdistä alueet" +"Merge the branch into the main file (one undo step) and archive it" = "Yhdistä haara päätiedostoon (yksi kumottava toiminto) ja arkistoi se" +"Merged" = "Yhdistetty" +"Merged PRs" = "Yhdistetyt PR:t" +"Min H" = "Vähimmäiskorkeus" +"Min W" = "Vähimmäisleveys" +"Minimize" = "Pienennä" +"Mirror angle" = "Peilaa kulma" +"Mirror angle and length" = "Peilaa kulma ja pituus" +"Mirroring" = "Peilaus" +"Missing" = "Puuttuu" +"Missing fonts" = "Puuttuvat fontit" +"Missing layer" = "Puuttuva taso" +"Missing style" = "Puuttuva tyyli" +"Missing variable" = "Puuttuva muuttuja" +"Miter" = "Jiiri" +"Miter angle" = "Jiirikulma" +"Mixed" = "Useita arvoja" +"Mixed fonts" = "Useita fontteja" +"Mixed interactions: click + to replace them" = "Vaihtelevia vuorovaikutuksia: korvaa napsauttamalla +" +"Mobile app" = "Mobiilisovellus" +"Mode" = "Tila" +"Model" = "Malli" +"Models" = "Mallit" +"Mono" = "Yksivärinen" +"More" = "Lisää" +"More instance actions" = "Lisää ilmentymän toimintoja" +"Mouse down" = "Hiiren painallus" +"Mouse enter" = "Osoitin saapuu" +"Mouse leave" = "Osoitin poistuu" +"Mouse up" = "Hiiren vapautus" +"Move" = "Siirrä" +"Move along the toolbar or the layer rows" = "Siirry työkalurivillä tai tasoriveillä" +"Move column left" = "Siirrä sarake vasemmalle" +"Move column right" = "Siirrä sarake oikealle" +"Move down" = "Siirrä alas" +"Move in" = "Siirry sisään" +"Move out" = "Siirry ulos" +"Move tools" = "Siirtotyökalut" +"Move up" = "Siirrä ylös" +"Moved to {name}" = "Siirretty kohteeseen {name}" +"Multi" = "Monivärinen" +"Multiply" = "Kerro" +"Muted" = "Hillitty" +"Name" = "Nimi" +"Name (A–Z)" = "Nimi (A–Z)" +"Named instance" = "Nimetty ilmentymä" +"Navigate to" = "Siirry kohteeseen" +"Nested instance" = "Sisäkkäinen ilmentymä" +"New design file" = "Uusi suunnittelutiedosto" +"Newest first" = "Uusimmat ensin" +"Next frame" = "Seuraava kehys" +"Next or previous control in the focused region" = "Seuraava tai edellinen säädin kohdistetulla alueella" +"Next ready for dev" = "Seuraava kehitysvalmis" +"Next size option" = "Seuraava kokovaihtoehto" +"No assets: no export settings, icons or images. Add export settings in the Design tab." = "Ei resursseja: ei vientiasetuksia, kuvakkeita tai kuvia. Lisää vientiasetuksia Suunnittelu-välilehdellä." +"No boolean variables fit here." = "Tähän ei sovi totuusarvomuuttujia." +"No boolean variables yet. Create them in Local variables." = "Ei vielä totuusarvomuuttujia. Luo niitä Paikalliset muuttujat -näkymässä." +"No branches. A branch is a copy of the file to try changes in, then review and merge." = "Ei haaroja. Haara on tiedoston kopio, jossa voi kokeilla muutoksia ja sitten tarkistaa ja yhdistää ne." +"No category" = "Ei luokkaa" +"No changes" = "Ei muutoksia" +"No color styles" = "Ei värityylejä" +"No colour variables fit here." = "Tähän ei sovi värimuuttujia." +"No colour variables yet. Create them in Local variables." = "Ei vielä värimuuttujia. Luo niitä Paikalliset muuttujat -näkymässä." +"No comments yet" = "Ei vielä kommentteja" +"No compatibility warnings reported" = "Yhteensopivuusvaroituksia ei ilmoitettu" +"No components yet. Select layers and press {keys}." = "Ei vielä komponentteja. Valitse tasoja ja paina {keys}." +"No conflicts." = "Ei ristiriitoja." +"No contributor data was built into this copy." = "Tähän versioon ei ole sisällytetty tekijätietoja." +"No device" = "Ei laitetta" +"No files or templates match your search" = "Hakua vastaavia tiedostoja tai malleja ei löydy" +"No fonts match" = "Vastaavia fontteja ei löydy" +"No join code here (the server is older than join codes, or this address can't be put in one): share the link instead." = "Liittymiskoodia ei ole (palvelin on liittymiskoodeja vanhempi tai tätä osoitetta ei voi sisällyttää koodiin). Jaa linkki sen sijaan." +"No layer changes to show." = "Ei näytettäviä tasomuutoksia." +"No libraries yet. Add a .figcraft file that was published as a library, or paste a team library's link." = "Ei vielä kirjastoja. Lisää kirjastona julkaistu .figcraft-tiedosto tai liitä tiimikirjaston linkki." +"No list" = "Ei luetteloa" +"No matching comments" = "Vastaavia kommentteja ei löydy" +"No matching components" = "Vastaavia komponentteja ei löydy" +"No matching layers" = "Vastaavia tasoja ei löydy" +"No matching styles" = "Vastaavia tyylejä ei löydy" +"No matching variables" = "Vastaavia muuttujia ei löydy" +"No mirroring" = "Ei peilausta" +"No model credits were built into this copy." = "Tähän versioon ei ole sisällytetty mallien tekijätietoja." +"No number variables fit here." = "Tähän ei sovi lukumuuttujia." +"No number variables yet. Create them in Local variables." = "Ei vielä lukumuuttujia. Luo niitä Paikalliset muuttujat -näkymässä." +"No other pages" = "Ei muita sivuja" +"No paints" = "Ei täyttöjä" +"No plugins on {server}." = "Palvelimella {server} ei ole liitännäisiä." +"No plugins yet. Import a development plugin from its figcraft-plugin.json." = "Ei vielä liitännäisiä. Tuo kehitysliitännäinen sen figcraft-plugin.json-tiedostosta." +"No preferred components" = "Ei ensisijaisia komponentteja" +"No preset" = "Ei esiasetusta" +"No saved versions yet. Save a version to mark a point in your work you can come back to." = "Ei vielä tallennettuja versioita. Tallenna versio, jotta voit palata tähän työvaiheeseen myöhemmin." +"No scrolling" = "Ei vieritystä" +"No shortcuts match" = "Vastaavia pikanäppäimiä ei löydy" +"No status" = "Ei tilaa" +"No string variables fit here." = "Tähän ei sovi merkkijonomuuttujia." +"No string variables yet. Create them in Local variables." = "Ei vielä merkkijonomuuttujia. Luo niitä Paikalliset muuttujat -näkymässä." +"No styles yet." = "Ei vielä tyylejä." +"No styles yet. Click + to create one from the selection." = "Ei vielä tyylejä. Luo tyyli valinnasta napsauttamalla +." +"No templates match your search" = "Hakua vastaavia malleja ei löydy" +"No text styles yet. Click + to create one from the selected text." = "Ei vielä tekstityylejä. Luo tyyli valitusta tekstistä napsauttamalla +." +"No variables fit here." = "Tähän ei sovi muuttujia." +"No variables yet. Create them in Local variables." = "Ei vielä muuttujia. Luo niitä Paikalliset muuttujat -näkymässä." +"Nobody else is here yet" = "Kukaan muu ei ole vielä paikalla" +"Noise" = "Kohina" +"None" = "Ei mitään" +"None." = "Ei mitään." +"Normal" = "Normaali" +"Not bound" = "Ei sidottu" +"Not in a live session" = "Ei reaaliaikaisessa istunnossa" +"Not in this font" = "Ei tässä fontissa" +"Not properties, kept as written: {names}" = "Eivät ole ominaisuuksia, säilytetään sellaisinaan: {names}" +"Not read yet" = "Ei vielä luettu" +"Not shared" = "Ei jaettu" +"Note {n}" = "Huomautus {n}" +"Nothing changed since this was marked ready for dev." = "Mikään ei ole muuttunut kehitysvalmiiksi merkitsemisen jälkeen." +"Nothing here yet. Set a frame's status to list it." = "Ei vielä sisältöä. Lisää kehys luetteloon asettamalla sen tila." +"Nothing online yet. Use Share on a file to put it online and it is listed here." = "Ei vielä verkkotiedostoja. Julkaise tiedosto verkkoon Jaa-toiminnolla, niin se näkyy tässä." +"Nothing to export" = "Ei vietävää" +"Nov" = "marrask." +"Now" = "Nyt" +"Nudge (Shift: big nudge)" = "Siirrä askel (Shift: suuri askel)" +"Nudge amount" = "Siirtoaskeleen koko" +"Number" = "Luku" +"Number ↑" = "Luku ↑" +"Number ↓" = "Luku ↓" +"Numbered list" = "Numeroitu luettelo" +"Object" = "Objekti" +"Oct" = "lokak." +"Off" = "Ei käytössä" +"Offered first when filling the slot (any content is allowed)" = "Ehdotetaan ensin sisältöpaikkaa täytettäessä (kaikki sisältö sallitaan)" +"Offered first when swapping the instance (any component is allowed)" = "Ehdotetaan ensin ilmentymää vaihdettaessa (kaikki komponentit sallitaan)" +"Offset" = "Siirtymä" +"Oldest first" = "Vanhimmat ensin" +"On click" = "Napsautettaessa" +"On drag" = "Vedettäessä" +"On this page" = "Tällä sivulla" +"One announcement in five common sizes: square, portrait, story, link and banner" = "Yksi ilmoitus viidessä tavallisessa koossa: neliö, pysty, tarina, linkki ja banneri" +"Online" = "Verkossa" +"Online files" = "Verkkotiedostot" +"Online sharing is off: files are shared on the local network only." = "Verkkojakaminen on pois käytöstä. Tiedostoja jaetaan vain lähiverkossa." +"Only the first changed layers are listed." = "Vain ensimmäiset muuttuneet tasot näytetään." +"Only the first {n} changed layers are listed." = "Vain ensimmäiset {n} muuttunutta tasoa näytetään." +"Only your threads" = "Vain omat keskustelut" +"Opacity" = "Peittävyys" +"Opacity & blend" = "Peittävyys ja sekoitus" +"Opacity 10%…90%, 0: 100%" = "Peittävyys 10 %…90 %, 0: 100 %" +"Open" = "Avaa" +"Open .figcraft, .svg or images" = "Avaa .figcraft-, .svg- tai kuvatiedostoja" +"Open branch" = "Avaa haara" +"Open file" = "Avaa tiedosto" +"Open files" = "Avaa tiedostoja" +"Open in focus view" = "Avaa kohdistusnäkymässä" +"Open in new window" = "Avaa uudessa ikkunassa" +"Open invite links in FigCraft" = "Avaa kutsulinkit FigCraftissa" +"Open link" = "Avaa linkki" +"Open link {url}" = "Avaa linkki {url}" +"Open overlay" = "Avaa peittokuva" +"Open prototype preview" = "Avaa prototyypin esikatselu" +"Open variables ({count})" = "Avaa muuttujat ({count})" +"Opened as a new document" = "Avattu uutena asiakirjana" +"Opened “{name}” in a new tab" = "”{name}” avattu uudessa välilehdessä" +"Opening the file…" = "Avataan tiedostoa…" +"Opening {n} files as new files" = { one = "Avataan uutena tiedostona", other = "Avataan uusia tiedostoja: {n}" } +"Open|comments" = "Avoin" +"Options" = "Asetukset" +"Orange" = "Oranssi" +"Order" = "Järjestys" +"Other" = "Muu" +"Other apps may not open every .fig file exported here." = "Muut sovellukset eivät välttämättä avaa kaikkia tässä vietyjä .fig-tiedostoja." +"Others lose access and keep their copy; the file stays on the server for you" = "Muiden käyttöoikeus päättyy, ja he säilyttävät kopionsa. Tiedosto jää palvelimelle käyttöösi." +"Others type this under Share ▸ Join with a code" = "Muut kirjoittavat tämän kohtaan Jaa ▸ Liity koodilla" +"Outline stroke" = "Muunna reunaviiva poluksi" +"Outline text" = "Muunna teksti poluiksi" +"Outlines" = "Ääriviivat" +"Outside" = "Ulkopuolella" +"Overflow" = "Ylivuoto" +"Overlay" = "Peittokuva" +"Overlay position" = "Peittokuvan sijainti" +"Overridden here (the collection it extends has another value)" = "Ohitettu tässä (laajennetussa kokoelmassa on toinen arvo)" +"Overrides" = "Ohitukset" +"Owner token" = "Omistajan tunniste" +"Owner token copied" = "Omistajan tunniste kopioitu" +"PDF print options" = "PDF-tulostusasetukset" +"PDF print settings" = "PDF-tulostusasetukset" +"Padding" = "Sisämarginaali" +"Page" = "Sivu" +"Pages" = "Sivut" +"Pages added to the document" = "Sivut lisätty asiakirjaan" +"Pages: {pages}" = "Sivut: {pages}" +"Paint bucket" = "Maalipurkki" +"Pan the canvas" = "Siirrä piirtoaluetta" +"Pan with nothing selected (Shift: farther)" = "Siirrä näkymää ilman valintaa (Shift: pidemmälle)" +"Panels" = "Paneelit" +"Paper" = "Paperi" +"Paragraph indent" = "Kappaleen sisennys" +"Paragraph indent applies to left-aligned text" = "Kappaleen sisennys koskee vasemmalle tasattua tekstiä" +"Paragraph spacing" = "Kappaleväli" +"Pass through" = "Läpivienti" +"Paste a link (https://…)" = "Liitä linkki (https://…)" +"Paste a team library link (a view link)" = "Liitä tiimikirjaston linkki (katselulinkki)" +"Paste here" = "Liitä tähän" +"Paste to replace" = "Liitä korvaten" +"Pasted design layers; {n} kinds of content were left out" = { one = "Suunnittelutasot liitetty; {n} sisältötyyppi jätettiin pois", other = "Suunnittelutasot liitetty; {n} sisältötyyppiä jätettiin pois" } +"Pattern" = "Kuvio" +"Pattern alignment" = "Kuvion tasaus" +"Pattern scale" = "Kuvion skaalaus" +"Pattern source" = "Kuvion lähde" +"Pen" = "Kynä" +"Pencil" = "Lyijykynä" +"People in this file" = "Tämän tiedoston käyttäjät" +"People join from anywhere with a code. The file is kept on {server} while you share it." = "Käyttäjät voivat liittyä koodilla mistä tahansa. Tiedosto säilytetään palvelimella {server} jakamisen ajan." +"People on your network join with a code. Nothing goes through a server." = "Lähiverkkosi käyttäjät liittyvät koodilla. Mitään ei kulje palvelimen kautta." +"Phone" = "Puhelin" +"Pick color" = "Poimi väri" +"Pick color (I)" = "Poimi väri (I)" +"Pick one font family first" = "Valitse ensin yksi fonttiperhe" +"Pick the Frame tool, then drag on the canvas or choose a device size" = "Valitse Kehys-työkalu ja vedä piirtoalueella tai valitse laitteen koko" +"Pin a property" = "Kiinnitä ominaisuus" +"Pinch" = "Nipistys" +"Pixel grid" = "Pikseliruudukko" +"Pixel preview 1x" = "Pikseliesikatselu 1x" +"Pixel preview 2x" = "Pikseliesikatselu 2x" +"Place a shape or frame (its tool chosen)" = "Sijoita muoto tai kehys (sen työkalu valittuna)" +"Place automatically" = "Sijoita automaattisesti" +"Place image" = "Sijoita kuva" +"Placed on the current page" = "Sijoitettu nykyiselle sivulle" +"Playground" = "Kokeilualue" +"Plugin" = "Liitännäinen" +"Plugins" = "Liitännäiset" +"Plugins & widgets" = "Liitännäiset ja pienoisohjelmat" +"Plugins run in FigCraft's sandbox, without files, network or clock. Install only plugins you trust." = "Liitännäiset toimivat FigCraftin eristetyssä ympäristössä ilman tiedostoja, verkkoa tai kelloa. Asenna vain luottamiasi liitännäisiä." +"Plus darker" = "Tummempi summa" +"Plus lighter" = "Vaaleampi summa" +"Point" = "Piste" +"Points" = "Pisteet" +"Polygon" = "Monikulmio" +"Polygons" = "Monikulmiot" +"Port" = "Portti" +"Position" = "Sijainti" +"Post" = "Lähetä" +"Preferences" = "Asetukset" +"Preferred" = "Ensisijainen" +"Present" = "Esitä" +"Present or preview" = "Esitä tai esikatsele" +"Present this flow" = "Esitä tämä kulku" +"Presentation" = "Esitys" +"Presentation slides" = "Esitysdiat" +"Presenting: comments" = "Esitys: kommentit" +"Presenting: cycle the size options" = "Esitys: kierrä kokovaihtoehtoja" +"Presenting: full screen" = "Esitys: koko näyttö" +"Presenting: next frame" = "Esitys: seuraava kehys" +"Presenting: previous frame" = "Esitys: edellinen kehys" +"Presenting: restart" = "Esitys: aloita uudelleen" +"Preserve scroll position" = "Säilytä vierityksen sijainti" +"Presets" = "Esiasetukset" +"Press a key…" = "Paina näppäintä…" +"Preview" = "Esikatselu" +"Preview this flow" = "Esikatsele tätä kulkua" +"Preview this frame" = "Esikatsele tätä kehystä" +"Previous frame" = "Edellinen kehys" +"Previous ready for dev" = "Edellinen kehitysvalmis" +"Progressive" = "Asteittainen" +"Properties" = "Ominaisuudet" +"Prototype" = "Prototyyppi" +"Prototype preview" = "Prototyypin esikatselu" +"Prototype settings" = "Prototyypin asetukset" +"Publish" = "Julkaise" +"Publish changes" = "Julkaise muutokset" +"Publish to another server…" = "Julkaise toiselle palvelimelle…" +"Publish to server" = "Julkaise palvelimelle" +"Published version {v}" = "Julkaistu versio {v}" +"Published version {v} to the server" = "Versio {v} julkaistu palvelimelle" +"Published version {v}: save the file so other files can use it" = "Versio {v} julkaistu: tallenna tiedosto, jotta muut tiedostot voivat käyttää sitä" +"Published · {people}" = "Julkaistu · {people}" +"Publishing to a server needs the desktop app; in the browser you can join a server's files" = "Palvelimelle julkaiseminen vaatii työpöytäsovelluksen. Selaimessa voit liittyä palvelimen tiedostoihin." +"Publishing with the server's key." = "Julkaistaan palvelimen avaimella." +"Publishing…" = "Julkaistaan…" +"Push" = "Työntö" +"Push changes to main component" = "Siirrä muutokset pääkomponenttiin" +"Put text on path" = "Sijoita teksti polulle" +"R" = "R" +"Radial" = "Säteittäinen" +"Radius" = "Säde" +"Ratio" = "Suhde" +"Reading the library…" = "Luetaan kirjastoa…" +"Ready for dev" = "Kehitysvalmis" +"Real name" = "Oikea nimi" +"Recent" = "Viimeisimmät" +"Recent files" = "Viimeksi avatut tiedostot" +"Recents" = "Viimeisimmät" +"Recovered {n} unsaved files after the app quit unexpectedly: save them to keep them" = { one = "Tallentamaton tiedosto palautettiin sovelluksen sulkeuduttua odottamatta. Tallenna se säilyttääksesi sen.", other = "Palautettu {n} tallentamatonta tiedostoa sovelluksen sulkeuduttua odottamatta. Tallenna ne säilyttääksesi ne." } +"Rectangle" = "Suorakulmio" +"Rectangles" = "Suorakulmiot" +"Red" = "Punainen" +"Reduce motion" = "Vähennä liikettä" +"Refraction" = "Taittuminen" +"Regular expression" = "Säännöllinen lauseke" +"Remove" = "Poista" +"Remove all" = "Poista kaikki" +"Remove annotation" = "Poista huomautus" +"Remove auto layout" = "Poista automaattinen asettelu" +"Remove dev resource" = "Poista kehittäjäresurssi" +"Remove export setting" = "Poista vientiasetus" +"Remove from recents" = "Poista viimeisimmistä" +"Remove from server" = "Poista palvelimelta" +"Remove from server…" = "Poista palvelimelta…" +"Remove guide" = "Poista apuviiva" +"Remove interaction" = "Poista vuorovaikutus" +"Remove library from this file" = "Poista kirjasto tästä tiedostosta" +"Remove min and max" = "Poista vähimmäis- ja enimmäisrajat" +"Remove min and max height" = "Poista korkeuden vähimmäis- ja enimmäisrajat" +"Remove min and max width" = "Poista leveyden vähimmäis- ja enimmäisrajat" +"Remove regions" = "Poista alueet" +"Remove slot" = "Poista sisältöpaikka" +"Remove starting point" = "Poista aloituspiste" +"Remove stop" = "Poista liukuvärin piste" +"Remove text from path" = "Poista teksti polulta" +"Remove the else and its actions" = "Poista muuten-haara ja sen toiminnot" +"Remove this condition and its actions" = "Poista tämä ehto ja sen toiminnot" +"Remove this file from the server? It can't be shared or opened from there again. Your copy stays open here." = "Poistetaanko tämä tiedosto palvelimelta? Sitä ei voi enää jakaa tai avata sieltä. Oma kopiosi pysyy auki tässä." +"Remove {name} from session" = "Poista {name} istunnosta" +"Remove {name} from {server}?" = "Poistetaanko {name} palvelimelta {server}?" +"Removed" = "Poistettu" +"Removed from the server" = "Poistettu palvelimelta" +"Removed since it was marked" = "Poistettu merkitsemisen jälkeen" +"Rename" = "Nimeä uudelleen" +"Rename duplicated layers" = "Nimeä monistetut tasot uudelleen" +"Rename folder" = "Nimeä kansio uudelleen" +"Rename group" = "Nimeä ryhmä uudelleen" +"Rename layers" = "Nimeä tasot uudelleen" +"Rename mode" = "Nimeä tila uudelleen" +"Rename on the server" = "Nimeä uudelleen palvelimella" +"Rename on the server…" = "Nimeä uudelleen palvelimella…" +"Rename page" = "Nimeä sivu uudelleen" +"Rename to" = "Uusi nimi" +"Replace all" = "Korvaa kaikki" +"Replace mixed export settings" = "Korvaa vaihtelevat vientiasetukset" +"Replace with" = "Korvaa kohteella" +"Replace with a new interaction" = "Korvaa uudella vuorovaikutuksella" +"Reply" = "Vastaa" +"Report" = "Ilmoita" +"Report this file…" = "Ilmoita tästä tiedostosta…" +"Reset" = "Palauta" +"Reset adjustments" = "Palauta säädöt" +"Reset all changes" = "Palauta kaikki muutokset" +"Reset all overrides on this layer" = "Palauta kaikki tämän tason ohitukset" +"Reset overrides" = "Palauta ohitukset" +"Reset slot" = "Palauta sisältöpaikka" +"Reset this override" = "Palauta tämä ohitus" +"Reset to style" = "Palauta tyylin arvo" +"Reset to the inherited value" = "Palauta peritty arvo" +"Resize by the nudge amount" = "Muuta kokoa siirtoaskeleen verran" +"Resize frames to the device's screen (or the window without a device): their constraints and auto layout respond" = "Sovita kehysten koko laitteen näyttöön (tai ikkunaan ilman laitetta). Rajoitteet ja automaattinen asettelu mukautuvat." +"Resize to fit" = "Sovita koko" +"Resizing" = "Koon muuttaminen" +"Resolve" = "Ratkaise" +"Resolve change" = "Ratkaise muutos" +"Resolved" = "Ratkaistu" +"Resources" = "Resurssit" +"Responsive" = "Mukautuva" +"Restart" = "Aloita uudelleen" +"Restore" = "Palauta" +"Restore main component" = "Palauta pääkomponentti" +"Restore this version" = "Palauta tämä versio" +"Restored “{name}”" = "”{name}” palautettu" +"Reverse the order" = "Käännä järjestys" +"Reversed triangle" = "Käänteinen kolmio" +"Review and merge" = "Tarkista ja yhdistä" +"Ribbon" = "Valintanauha" +"Right" = "Oikea" +"Right side" = "Oikea puoli" +"Right side width" = "Oikean puolen leveys" +"Rotate 90°" = "Kierrä 90°" +"Rotate 90° right" = "Kierrä 90° oikealle" +"Rotate to landscape" = "Käännä vaakasuuntaan" +"Rotate to portrait" = "Käännä pystysuuntaan" +"Rotation" = "Kierto" +"Rotation jitter" = "Kierron vaihtelu" +"Round" = "Pyöreä" +"Rounded" = "Pyöristetty" +"Row" = "Rivi" +"Row gap" = "Rivien väli" +"Row sizes" = "Rivien koot" +"Row span" = "Riviulottuvuus" +"Rows" = "Rivit" +"Rows {count}" = "Rivejä: {count}" +"Rulers" = "Viivaimet" +"Run" = "Suorita" +"Run {label}" = "Suorita {label}" +"SHA-256: {sha}…" = "SHA-256: {sha}…" +"Same as layer" = "Sama kuin tasolla" +"Saturation" = "Kylläisyys" +"Save" = "Tallenna" +"Save as" = "Tallenna nimellä" +"Save as preset…" = "Tallenna esiasetuksena…" +"Save files automatically" = "Tallenna tiedostot automaattisesti" +"Save version {key}" = "Tallenna versio {key}" +"Saved" = "Tallennettu" +"Saved {name}" = "Tallennettu: {name}" +"Say something" = "Kirjoita viesti" +"Scale" = "Skaalaus" +"Scatter" = "Hajonta" +"Scoping: where pickers offer it" = "Käyttöalue: missä valitsimet tarjoavat tätä" +"Screen" = "Rasteri" +"Scroll behavior" = "Vierityksen toiminta" +"Scroll to" = "Vieritä kohteeseen" +"Scroll to layer" = "Vieritä tasoon" +"Scroll with parent" = "Vieritä emotason mukana" +"Search" = "Etsi" +"Search actions, layers and tools" = "Etsi toimintoja, tasoja ja työkaluja" +"Search assets" = "Etsi resursseja" +"Search comments" = "Etsi kommentteja" +"Search files and templates" = "Etsi tiedostoja ja malleja" +"Search fonts" = "Etsi fontteja" +"Search layers" = "Etsi tasoja" +"Search plugins" = "Etsi liitännäisiä" +"Search results" = "Hakutulokset" +"Search shortcuts" = "Etsi pikanäppäimiä" +"Search templates" = "Etsi malleja" +"Section" = "Osio" +"Section color" = "Osion väri" +"Sections" = "Osiot" +"See all templates" = "Näytä kaikki mallit" +"Select a frame and add a starting point to present it as a flow." = "Valitse kehys ja lisää aloituspiste, jotta voit esittää sen kulkuna." +"Select a frame to preview it" = "Valitse kehys esikatseltavaksi" +"Select a layer to see its properties and code" = "Valitse taso, jotta näet sen ominaisuudet ja koodin" +"Select all" = "Valitse kaikki" +"Select all with same" = "Valitse kaikki samalla ominaisuudella" +"Select layer" = "Valitse taso" +"Select layers and they space themselves as their content changes" = "Valitse tasoja, niin niiden välit mukautuvat sisällön muuttuessa" +"Select layers to apply the style to" = "Valitse tasot, joihin tyyliä käytetään" +"Select layers with this color" = "Valitse tämänväriset tasot" +"Select overlapping shapes" = "Valitse päällekkäiset muodot" +"Select points to edit their position, radius and mirroring" = "Valitse pisteitä muokataksesi niiden sijaintia, sädettä ja peilausta" +"Select slot" = "Valitse sisältöpaikka" +"Selection" = "Valinta" +"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Valintalaatikko, {w} × {h} kohdassa {x}, {y}: nuolet siirtävät, Shift+nuolet tai {cmd}+nuolet muuttavat kokoa, Enter valitsee, Escape peruuttaa" +"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Valintalaatikko: nuolet siirtävät, Shift+nuolet tai {cmd}+nuolet muuttavat kokoa, Enter valitsee, Escape peruuttaa" +"Selection colors" = "Valinnan värit" +"Send backward" = "Vie taaksepäin" +"Send report" = "Lähetä ilmoitus" +"Send to back" = "Vie takimmaiseksi" +"Sep" = "syysk." +"Server address" = "Palvelimen osoite" +"Server address (ws://host:47900/)" = "Palvelimen osoite (ws://host:47900/)" +"Server for online sharing" = "Verkkojakamisen palvelin" +"Set as default" = "Aseta oletukseksi" +"Set variable" = "Aseta muuttuja" +"Set variable mode" = "Aseta muuttujan tila" +"Set variable {name}" = "Aseta muuttuja {name}" +"Set {name} to {value}" = "Aseta {name} arvoon {value}" +"Shadows" = "Varjot" +"Shape" = "Muoto" +"Shape builder" = "Muodonrakennin" +"Shape tools" = "Muototyökalut" +"Share" = "Jaa" +"Share this file" = "Jaa tämä tiedosto" +"Share with a code" = "Jaa koodilla" +"Share: live session, export" = "Jaa: reaaliaikainen istunto, vienti" +"Shared" = "Jaettu" +"Shared: copy its code from the file's menu" = "Jaettu: kopioi koodi tiedoston valikosta" +"Sharing a file needs the desktop app; here you can join one." = "Tiedoston jakaminen vaatii työpöytäsovelluksen. Tässä voit liittyä jaettuun tiedostoon." +"Sharing on the local network (online sharing is off: no server is set in Preferences)." = "Jaetaan lähiverkossa (verkkojakaminen on pois käytöstä: asetuksissa ei ole palvelinta)." +"Sharing on your network · {people}" = "Jaetaan lähiverkossasi · {people}" +"Sharing online · {people}" = "Jaetaan verkossa · {people}" +"Sharing stopped: the file stays on the server, open only to you" = "Jakaminen lopetettu: tiedosto jää palvelimelle vain sinun käyttöösi" +"Show" = "Näytä" +"Show all templates" = "Näytä kaikki mallit" +"Show as grid" = "Näytä ruudukkona" +"Show as list" = "Näytä luettelona" +"Show autosaves" = "Näytä automaattiset tallennukset" +"Show behind transparent areas" = "Näytä läpinäkyvien alueiden takana" +"Show comments (Shift+C)" = "Näytä kommentit (Shift+C)" +"Show device frame" = "Näytä laitteen kehys" +"Show dimensions on objects" = "Näytä objektien mitat" +"Show hotspot hints" = "Näytä aktiivisten kohtien vihjeet" +"Show in Explorer" = "Näytä Resurssienhallinnassa" +"Show in Finder" = "Näytä Finderissa" +"Show in folder" = "Näytä kansiossa" +"Show only this on the canvas" = "Näytä piirtoalueella vain tämä" +"Show open" = "Näytä avoimet" +"Show section contents" = "Näytä osion sisältö" +"Show this layer" = "Näytä tämä taso" +"Show usage" = "Näytä käyttö" +"Show/Hide" = "Näytä/kätke" +"Show/Hide UI" = "Näytä/kätke käyttöliittymä" +"Side by side" = "Vierekkäin" +"Sidebar" = "Sivupalkki" +"Simplify stroke" = "Yksinkertaista reunaviiva" +"Size" = "Koko" +"Size jitter" = "Koon vaihtelu" +"Skip ink" = "Väistä merkkien viivoja" +"Slashed zero" = "Viivallinen nolla" +"Slice" = "Viipale" +"Slices" = "Viipaleet" +"Slide in" = "Liu'u sisään" +"Slide out" = "Liu'u ulos" +"Slot" = "Sisältöpaikka" +"Slot content" = "Sisältöpaikan sisältö" +"Small capitals" = "Kapiteelit" +"Small caps" = "Kapiteelit" +"Smart animate" = "Älykäs animointi" +"Snap to pixel grid" = "Tartu pikseliruudukkoon" +"Social and slides" = "Some ja diat" +"Social media" = "Sosiaalinen media" +"Social post sizes" = "Somejulkaisujen koot" +"Soft light" = "Pehmeä valo" +"Solid" = "Yksivärinen" +"Some variants have the same property values: give each one a unique combination" = "Joillakin varianteilla on samat ominaisuusarvot. Anna kullekin yksilöllinen yhdistelmä." +"Sort" = "Lajittele" +"Sort by" = "Lajitteluperuste" +"Sort by date" = "Lajittele päivämäärän mukaan" +"Sort by page" = "Lajittele sivun mukaan" +"Sort by {column}" = "Lajittele sarakkeen {column} mukaan" +"Space around" = "Tila ympärillä" +"Space between" = "Tila välissä" +"Space drag" = "Space drag" +"Space evenly" = "Tasavälein" +"Space+drag" = "Space+drag" +"Spacing" = "Välistys" +"Spam, stolen work, abuse…" = "Roskaposti, varastettu työ, väärinkäyttö…" +"Splay" = "Harotus" +"Spotlight me" = "Nosta minut esiin" +"Spread" = "Leviäminen" +"Square" = "Neliö" +"Standard" = "Vakio" +"Standard ligatures" = "Tavalliset ligatuurit" +"Star" = "Tähti" +"Stars" = "Tähdet" +"Start" = "Alku" +"Start ascending from / stop descending at" = "Nousevan osan alku / laskevan osan loppu" +"Start from a blank canvas" = "Aloita tyhjältä piirtoalueelta" +"Start from a template" = "Aloita mallista" +"Start on path" = "Aloituskohta polulla" +"Start point" = "Alkupiste" +"Start sharing" = "Aloita jakaminen" +"Start sharing with the Share button, then pass on the code it gives" = "Aloita jakaminen Jaa-painikkeella ja välitä sen antama koodi" +"Starting to share…" = "Aloitetaan jakamista…" +"Status" = "Tila" +"Sticky" = "Kiinnittyvä" +"Stop" = "Pysäytä" +"Stop following {name}" = "Lopeta käyttäjän {name} seuraaminen" +"Stop session" = "Lopeta istunto" +"Stop sharing" = "Lopeta jakaminen" +"Stop spotlighting" = "Lopeta esiin nostaminen" +"Stop {n} {hex}" = "Piste {n} {hex}" +"Stretch" = "Venytys" +"Strikethrough" = "Yliviivaus" +"String" = "Merkkijono" +"Stroke" = "Reunaviiva" +"Stroke style" = "Reunaviivan tyyli" +"Stroke weight" = "Reunaviivan paksuus" +"Strokes" = "Reunaviivat" +"Style" = "Tyyli" +"Styles" = "Tyylit" +"Stylistic set 1" = "Tyylijoukko 1" +"Stylistic set 2" = "Tyylijoukko 2" +"Stylistic set 3" = "Tyylijoukko 3" +"Stylistic set 4" = "Tyylijoukko 4" +"Stylistic set 5" = "Tyylijoukko 5" +"Subtract" = "Vähennä" +"Subtracts" = "Vähentää" +"Suffix" = "Jälkiliite" +"Suspend snapping while dragging" = "Keskeytä tarttuminen vetämisen ajaksi" +"Swap instance" = "Vaihda ilmentymä" +"Swap library: use another library's assets of the same names" = "Vaihda kirjasto: käytä toisen kirjaston samannimisiä resursseja" +"Swap orientation" = "Vaihda suunta" +"Swap overlay" = "Vaihda peittokuva" +"Swap to {name}" = "Vaihda kohteeseen {name}" +"Swap width and height" = "Vaihda leveys ja korkeus keskenään" +"Swapped instance" = "Ilmentymä vaihdettu" +"Swapped {n} assets" = { one = "Vaihdettu {n} resurssi", other = "Vaihdettu {n} resurssia" } +"Swapped {n} assets; {missed} had no match and still use the old library" = { one = "Vaihdettu {n} resurssi; ilman vastinetta jäi {missed}, ja ne käyttävät yhä vanhaa kirjastoa", other = "Vaihdettu {n} resurssia; ilman vastinetta jäi {missed}, ja ne käyttävät yhä vanhaa kirjastoa" } +"Sweep" = "Pyyhkäisy" +"System" = "Järjestelmä" +"Tab / Shift+Tab" = "Tab / Shift+Tab" +"Table" = "Taulukko" +"Tablet" = "Tabletti" +"Tabular numbers" = "Tasalevyiset numerot" +"Take the values from the first selected layer" = "Ota arvot ensimmäisestä valitusta tasosta" +"Taper" = "Kapeneminen" +"Taper both ends" = "Kavenna molemmat päät" +"Tell the server's operator what is wrong with it" = "Kerro palvelimen ylläpitäjälle, mikä siinä on vikana" +"Temperature" = "Lämpötila" +"Template" = "Malli" +"Templates" = "Mallit" +"Terms" = "Ehdot" +"Text" = "Teksti" +"Text align" = "Tekstin tasaus" +"Text content" = "Tekstisisältö" +"Text style" = "Tekstityyli" +"Text styles" = "Tekstityylit" +"Texts" = "Tekstit" +"Texture" = "Tekstuuri" +"The canvas previews colors in sRGB; exported PNG, JPG and WebP files are tagged Display P3." = "Piirtoalue esikatselee värit sRGB-muodossa. Vietäviin PNG-, JPG- ja WebP-tiedostoihin merkitään Display P3 -profiili." +"The destination document is unavailable. Nothing was imported." = "Kohdeasiakirja ei ole käytettävissä. Mitään ei tuotu." +"The destination document was closed. Nothing was imported." = "Kohdeasiakirja suljettiin. Mitään ei tuotu." +"The destination page was removed. Nothing was imported." = "Kohdesivu poistettiin. Mitään ei tuotu." +"The file is deleted from the server: nobody can open or join it from there again, and this can't be undone. Copies that are open stay with whoever has them." = "Tiedosto poistetaan palvelimelta. Kukaan ei voi enää avata sitä tai liittyä siihen sieltä, eikä tätä voi kumota. Avoimet kopiot säilyvät käyttäjillään." +"The file now holds another library" = "Tiedosto sisältää nyt toisen kirjaston" +"The file now holds another library (\"{name}\")" = "Tiedosto sisältää nyt toisen kirjaston (”{name}”)" +"The files you shared or published online from this computer. They stay on their server until you remove them." = "Tältä tietokoneelta verkossa jakamasi tai julkaisemasi tiedostot. Ne säilyvät palvelimellaan, kunnes poistat ne." +"The link carries the session's secret: share it only with people you invite." = "Linkki sisältää istunnon salaisen tunnisteen. Jaa se vain kutsumillesi henkilöille." +"The name in your online files and on the server. The document inside keeps its own name." = "Nimi verkkotiedostoissasi ja palvelimella. Sisällä oleva asiakirja säilyttää oman nimensä." +"The selection uses several text styles: click to apply one" = "Valinta käyttää useita tekstityylejä. Käytä yhtä niistä napsauttamalla." +"The server deletes this file if nobody opens it before {date}." = "Palvelin poistaa tämän tiedoston, ellei kukaan avaa sitä ennen päivää {date}." +"The whole file" = "Koko tiedosto" +"Theme" = "Teema" +"There's no destination for that action" = "Toiminnolla ei ole kohdetta" +"Thickness" = "Paksuus" +"This branch can't be reviewed: {e}" = "Tätä haaraa ei voi tarkistaa: {e}" +"This branch was merged or archived." = "Tämä haara on yhdistetty tai arkistoitu." +"This computer only" = "Vain tämä tietokone" +"This computer remembers the owner token, so the file can be managed from the home screen's Online list. Copy it to manage the file from somewhere else: it is a password for this file and won't be shown again." = "Tämä tietokone muistaa omistajan tunnisteen, joten tiedostoa voi hallita aloitusnäkymän Verkossa-luettelosta. Kopioi tunniste hallitaksesi tiedostoa muualla. Se on tämän tiedoston salasana, eikä sitä näytetä uudelleen." +"This condition is edited as an expression" = "Tätä ehtoa muokataan lausekkeena" +"This file" = "Tämä tiedosto" +"This file has unsaved changes" = "Tässä tiedostossa on tallentamattomia muutoksia" +"This file is on {server}, open only to you. Share it to let others in with a new code." = "Tämä tiedosto on palvelimella {server} vain sinun käytössäsi. Jaa se, jotta muut voivat liittyä uudella koodilla." +"This file uses fonts that aren't installed on this computer. Their text is drawn in a fallback font until you install them." = "Tämä tiedosto käyttää fontteja, joita ei ole asennettu tälle tietokoneelle. Teksti piirretään korvaavalla fontilla, kunnes asennat puuttuvat fontit." +"This frame is new on the branch." = "Tämä kehys on uusi haarassa." +"This frame isn't on the branch." = "Tätä kehystä ei ole haarassa." +"This server is open to the public: no key needed." = "Tämä palvelin on julkinen: avainta ei tarvita." +"This server needs its key: add ?key=… to the address (`figcraft-server key` prints it on the server's machine)." = "Tämä palvelin tarvitsee avaimen. Lisää osoitteeseen ?key=… (`figcraft-server key` tulostaa avaimen palvelimen koneella)." +"This server takes files only with its key: see Advanced." = "Tämä palvelin ottaa tiedostoja vastaan vain avaimellaan. Katso Lisäasetukset." +"Tidy up" = "Järjestä tasavälein" +"Tile" = "Laatoitus" +"Tile type" = "Laatan tyyppi" +"Tint" = "Sävytys" +"Title" = "Otsikko" +"Title case" = "Sanojen isot alkukirjaimet" +"To share this library with your team, publish the file to a server (Share ▸ Publish to server…), then publish the library there." = "Jaa tämä kirjasto tiimillesi julkaisemalla tiedosto palvelimelle (Jaa ▸ Julkaise palvelimelle…) ja julkaisemalla kirjasto siellä." +"Toggle" = "Vaihda tila" +"Toggle light/dark theme" = "Vaihda vaalea/tumma teema" +"Toggle visibility" = "Vaihda näkyvyys" +"Tools" = "Työkalut" +"Top" = "Ylhäältä" +"Top & bottom" = "Ylä- ja alareuna" +"Top left radius" = "Vasemman yläkulman säde" +"Top right radius" = "Oikean yläkulman säde" +"Transform" = "Muunna" +"Triangle arrow" = "Kolmionuoli" +"Trigger" = "Laukaisin" +"True" = "Tosi" +"Truncate text" = "Lyhennä tekstiä" +"Truncate with an ellipsis" = "Lyhennä kolmella pisteellä" +"Try again" = "Yritä uudelleen" +"Try another search or filter." = "Kokeile toista hakua tai suodatinta." +"Try another search, or browse templates." = "Kokeile toista hakua tai selaa malleja." +"Try another word, or look at every kind." = "Kokeile toista sanaa tai katso kaikki tyypit." +"Turn a layer into a component, reuse it, and change every copy at once" = "Muunna taso komponentiksi, käytä sitä uudelleen ja muuta kaikkia kopioita kerralla" +"Two digits" = "Kaksi numeroa" +"Type" = "Teksti" +"Type settings" = "Tekstiasetukset" +"Type the join code you were given, or paste an invite link." = "Kirjoita saamasi liittymiskoodi tai liitä kutsulinkki." +"Typography" = "Typografia" +"Underline" = "Alleviivaus" +"Undo takes back only your own changes; what others changed since stays." = "Kumoaminen peruu vain omat muutoksesi. Muiden sen jälkeen tekemät muutokset säilyvät." +"Ungroup" = "Pura ryhmittely" +"Uniform" = "Tasainen" +"Union" = "Yhdiste" +"Unions" = "Yhdistää" +"Unknown — treated as sRGB" = "Tuntematon — käsitellään sRGB:nä" +"Unread" = "Lukematon" +"Unresolve" = "Merkitse ratkaisemattomaksi" +"Unsaved changes" = "Tallentamattomat muutokset" +"Unsaved changes in {names}. Close the window and lose them?" = "Tallentamattomia muutoksia kohteissa {names}. Suljetaanko ikkuna ja hylätään ne?" +"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Tallentamattomia muutoksia: tallenna ensin ({save}) tai hylkää sulkemalla välilehti {alt}-napsautuksella" +"Unsaved changes: save first, or close again with {key} to discard" = "Tallentamattomia muutoksia: tallenna ensin tai hylkää sulkemalla uudelleen näppäimellä {key}" +"Up" = "Ylös" +"Update" = "Päivitä" +"Update from main" = "Päivitä päähaarasta" +"Update from selection" = "Päivitä valinnasta" +"Update plugin" = "Päivitä liitännäinen" +"Updated {components} and {styles}" = "Päivitetty {components} ja {styles}" +"Updated {name}" = "Päivitetty: {name}" +"Updating the graphics driver often helps. To use OpenGL only, start FigCraft from a Command Prompt with:" = "Näytönohjainajurin päivittäminen auttaa usein. Käytä vain OpenGL:ää käynnistämällä FigCraft komentokehotteesta näin:" +"Updating the graphics driver often helps. WGPU_BACKEND (vulkan, gl, metal) names another graphics API to try." = "Näytönohjainajurin päivittäminen auttaa usein. WGPU_BACKEND (vulkan, gl, metal) määrittää toisen kokeiltavan grafiikkarajapinnan." +"Uploads this file to a FigCraft server and keeps this tab in sync with it. You get a link to edit and a link to view." = "Lataa tämän tiedoston FigCraft-palvelimelle ja pitää tämän välilehden synkronoituna sen kanssa. Saat muokkauslinkin ja katselulinkin." +"Uppercase" = "Isot kirjaimet" +"Use a fixed value" = "Käytä kiinteää arvoa" +"Use an expression, e.g. count + 1" = "Käytä lauseketta, esim. count + 1" +"Use as mask" = "Käytä maskina" +"Use auto layout" = "Käytä automaattista asettelua" +"Use number keys for opacity" = "Säädä peittävyyttä numeronäppäimillä" +"Use the .figcraft or .fig extension." = "Käytä .figcraft- tai .fig-tiedostopäätettä." +"Use the layer's fill" = "Käytä tason täyttöä" +"Used in {files} · {instances}" = "Käytössä tiedostoissa {files} · {instances}" +"Username" = "Käyttäjänimi" +"Uses variables: each layer shows them in its own mode" = "Käyttää muuttujia: kukin taso näyttää ne omassa tilassaan" +"Value" = "Arvo" +"Variable" = "Muuttuja" +"Variable collections" = "Muuttujakokoelmat" +"Variable modes" = "Muuttujatilat" +"Variable width" = "Vaihteleva leveys" +"Variables" = "Muuttujat" +"Variables JSON…" = "Muuttujat JSON…" +"Variables store reusable values (colours, numbers, strings, booleans) with modes such as Light and Dark." = "Muuttujat tallentavat uudelleenkäytettäviä arvoja (värejä, lukuja, merkkijonoja ja totuusarvoja). Niillä voi olla tiloja, kuten vaalea ja tumma." +"Variant" = "Variantti" +"Vector" = "Vektori" +"Vectors" = "Vektorit" +"Version" = "Versio" +"Version history" = "Versiohistoria" +"Version {version}" = "Versio {version}" +"Version {v} · {components} · {styles} · {variables}" = "Versio {v} · {components} · {styles} · {variables}" +"Vertical" = "Pystysuunta" +"Vertical align" = "Pystytasaus" +"Vertical hex" = "Pystysuuntainen kuusikulmio" +"Vertical padding" = "Pystysuuntainen sisämarginaali" +"Vertical spacing" = "Pystyvälistys" +"Vertical trim" = "Pystysuuntainen rajaus" +"View" = "Näytä" +"View link" = "Katselulinkki" +"View link copied" = "Katselulinkki kopioitu" +"View only" = "Vain katselu" +"View only: you see every change as it happens but can't edit. Leave to keep a copy you can edit." = "Vain katselu: näet kaikki muutokset reaaliajassa, mutta et voi muokata. Poistu säilyttääksesi muokattavan kopion." +"Viewing “{name}”" = "Katsellaan tiedostoa ”{name}”" +"Violet" = "Violetti" +"Visibility" = "Näkyvyys" +"Visible" = "Näkyvä" +"Vivid" = "Kirkas" +"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND on asetettu ja määrittää käytettävän grafiikkarajapinnan. Poista se käytöstä, jotta FigCraft voi valita." +"Warm" = "Lämmin" +"Watch" = "Seuraa" +"Wavy" = "Aaltoileva" +"Web" = "Verkko" +"WebSocket URL" = "WebSocket-URL" +"WebSocket URL copied" = "WebSocket-URL kopioitu" +"Websites" = "Verkkosivustot" +"Wedge" = "Kiila" +"Weight" = "Paksuus" +"What changed since it was marked ready for dev" = "Muutokset kehitysvalmiiksi merkitsemisen jälkeen" +"What is wrong with this file? The server's operator gets your report." = "Mikä tässä tiedostossa on vikana? Palvelimen ylläpitäjä saa ilmoituksesi." +"What it's for" = "Käyttötarkoitus" +"What this changed from isn't known: it changed before FigCraft kept a copy of ready-for-dev frames. Resolve the change (or mark it ready again) to compare from now on." = "Aiempi tila ei ole tiedossa: muutos tehtiin ennen kuin FigCraft tallensi kopioita kehitysvalmiista kehyksistä. Ratkaise muutos tai merkitse kehys uudelleen valmiiksi, jotta jatkossa vertailu onnistuu." +"What you try here changes this preview and the code below, not the file" = "Tässä kokeilemasi asiat muuttavat esikatselua ja alla olevaa koodia, eivät tiedostoa" +"While hovering" = "Osoitettaessa" +"While pressing" = "Painettaessa" +"White" = "Valkoinen" +"Who runs it, its terms and how long it keeps files" = "Ylläpitäjä, käyttöehdot ja tiedostojen säilytysaika" +"Why it changed" = "Muutoksen syy" +"Widget" = "Pienoisohjelma" +"Widgets" = "Pienoisohjelmat" +"Width" = "Leveys" +"Width point" = "Leveyspiste" +"Width profile" = "Leveysprofiili" +"Wireframe kit" = "Rautalankapaketti" +"With the layers' export settings" = "Tasojen vientiasetuksilla" +"Wrap" = "Rivitys" +"Wrap in new section" = "Ympäröi uudella osiolla" +"Yellow" = "Keltainen" +"Yesterday" = "Eilen" +"You" = "Sinä" +"You'll get the shared file as a new tab and edit it together." = "Jaettu tiedosto avautuu uuteen välilehteen, ja voitte muokata sitä yhdessä." +"You're spotlighting: everyone follows your view" = "Olet esillä: kaikki seuraavat näkymääsi" +"Your copy of the file stays open" = "Oma kopiosi tiedostosta pysyy auki" +"Your copy stays open; the file stays on the server" = "Oma kopiosi pysyy auki, ja tiedosto jää palvelimelle" +"Your name" = "Nimesi" +"Your name in comments" = "Nimesi kommenteissa" +"Your system asks for reduced motion: animations stay off." = "Järjestelmäsi pyytää vähennettyä liikettä. Animaatiot pidetään pois käytöstä." +"Z" = "Z" +"Z to A" = "Z:sta A:han" +"Zoom" = "Zoomaus" +"Zoom around the pointer" = "Zoomaa osoittimen ympärillä" +"Zoom device to fill window" = "Zoomaa laite ikkunan kokoiseksi" +"Zoom in" = "Lähennä" +"Zoom out" = "Loitonna" +"Zoom to 100%" = "Zoomaa kokoon 100 %" +"Zoom to 200%" = "Zoomaa kokoon 200 %" +"Zoom to 50%" = "Zoomaa kokoon 50 %" +"Zoom to fit" = "Sovita näkymään" +"Zoom to selection" = "Zoomaa valintaan" +"Zoom tool: click to zoom in, drag an area" = "Zoomaustyökalu: lähennä napsauttamalla tai vedä alue" +"Zoom tool: zoom out" = "Zoomaustyökalu: loitonna" +"and {n} more" = { one = "ja vielä {n}", other = "ja vielä {n}" } +"bleed marks" = "leikkuuvaramerkit" +"by {author}" = "tekijä: {author}" +"call any command its access allows" = "kutsua kaikkia käyttöoikeuksiensa sallimia komentoja" +"call only: {commands}" = "kutsua vain: {commands}" +"can edit the document" = "voi muokata asiakirjaa" +"conditional prototype actions nested too deeply were skipped" = "liian syvälle sisäkkäiset ehdolliset prototyyppitoiminnot ohitettiin" +"conditional prototype actions with conditions that can't be evaluated were skipped" = "ehdolliset prototyyppitoiminnot ohitettiin, koska niiden ehtoja ei voi laskea" +"connector label background was skipped: style data is missing" = "yhdistimen tekstin tausta ohitettiin: tyylitiedot puuttuvat" +"connector label was skipped: background bounds are invalid" = "yhdistimen teksti ohitettiin: taustan rajat ovat virheelliset" +"connector label was skipped: background transform is invalid" = "yhdistimen teksti ohitettiin: taustan muunnos on virheellinen" +"connector label was skipped: composed text transform is invalid" = "yhdistimen teksti ohitettiin: yhdistetty tekstimuunnos on virheellinen" +"connector label was skipped: derived background bounds are missing" = "yhdistimen teksti ohitettiin: johdetut taustan rajat puuttuvat" +"connector label was skipped: derived text bounds are missing" = "yhdistimen teksti ohitettiin: johdetut tekstin rajat puuttuvat" +"connector label was skipped: text bounds are invalid" = "yhdistimen teksti ohitettiin: tekstin rajat ovat virheelliset" +"connector label was skipped: text style data is missing" = "yhdistimen teksti ohitettiin: tekstin tyylitiedot puuttuvat" +"connector label was skipped: text transform is invalid" = "yhdistimen teksti ohitettiin: tekstin muunnos on virheellinen" +"connector labels were skipped" = "yhdistimien tekstit ohitettiin" +"connector layers import as static frame/vector snapshots with labels when present; live endpoint bindings are not retained" = "yhdistintasot tuodaan staattisina kehys- ja vektorikuvina mahdollisine teksteineen; päiden reaaliaikaisia sidoksia ei säilytetä" +"connector snapshot vector data is missing; connector was left empty" = "yhdistimen vektorikuvatiedot puuttuvat; yhdistin jätettiin tyhjäksi" +"connectors whose ends can't be placed are empty" = "yhdistimet, joiden päitä ei voi sijoittaa, ovat tyhjiä" +"crop marks" = "leikkausmerkit" +"damaged vector geometry was skipped" = "vaurioitunut vektorigeometria ohitettiin" +"damaged vector networks import as outlines" = "vaurioituneet vektoriverkot tuodaan ääriviivoina" +"development: {path}" = "kehitys: {path}" +"document children other than pages were skipped" = "asiakirjan muut kuin sivuina olevat alikohteet ohitettiin" +"duplicate grid track sizing entries use equal tracks" = "päällekkäiset ruudukon kokomääritykset käyttävät tasakokoisia osia" +"e.g. count + 1" = "esim. count + 1" +"e.g. count >= 3 and not done" = "esim. count >= 3 and not done" +"elbowed and curved connectors without a saved path import as straight lines" = "mutkaiset ja kaarevat yhdistimet, joilla ei ole tallennettua polkua, tuodaan suorina viivoina" +"entries without a node type were skipped" = "kohteet, joilta puuttuu solmutyyppi, ohitettiin" +"figcraft:// links from a browser, chat or email open this app and join the session." = "selaimen, keskustelun tai sähköpostin figcraft://-linkit avaavat tämän sovelluksen ja liittyvät istuntoon." +"filled areas of a vector with their own fill use the layer's fill" = "vektorin omalla täytöllä määritetyt alueet käyttävät tason täyttöä" +"from {server}" = "palvelimelta {server}" +"gamepad triggers without exactly one known controller button were skipped" = "peliohjainlaukaisimet, joilla ei ole täsmälleen yhtä tunnettua ohjainpainiketta, ohitettiin" +"grid track list exceeds 1000 entries; the tracks use the default equal sizing" = "ruudukon osien luettelo ylittää 1000 kohdetta; osat käyttävät oletuksen mukaista tasakokoa" +"grid track sizing is unsupported or malformed; affected tracks use equal sizing" = "ruudukon kokomääritys on virheellinen tai sitä ei tueta; kyseiset osat käyttävät tasakokoa" +"grid track sizing list exceeds 1000 entries; the tracks use the default equal sizing" = "ruudukon kokomääritysten luettelo ylittää 1000 kohdetta; osat käyttävät oletuksen mukaista tasakokoa" +"hidden" = "kätketty" +"iOS" = "iOS" +"image filters are ambiguous because both filter containers are present; filter conversion was skipped" = "kuvasuotimet ovat monitulkintaisia, koska molemmat suodinsäilöt ovat läsnä; suotimien muunnos ohitettiin" +"image filters use FigCraft's approximate renderer" = "kuvasuotimet käyttävät FigCraftin likimääräistä renderöijää" +"instance swap properties whose default component isn't in the file were skipped" = "ilmentymän vaihto-ominaisuudet ohitettiin, koska niiden oletuskomponenttia ei ole tiedostossa" +"instances inside their own main component were imported as frames" = "oman pääkomponenttinsa sisällä olevat ilmentymät tuotiin kehyksinä" +"instances of components that aren't in the file show no layers" = "tiedostosta puuttuvien komponenttien ilmentymät eivät näytä tasoja" +"instances of components that aren't in the file were imported as frames" = "tiedostosta puuttuvien komponenttien ilmentymät tuotiin kehyksinä" +"instances that would nest a component inside itself were imported as frames" = "ilmentymät, jotka sijoittaisivat komponentin itsensä sisään, tuotiin kehyksinä" +"instances without a main component were imported as empty frames" = "ilmentymät, joilta puuttuu pääkomponentti, tuotiin tyhjinä kehyksinä" +"invalid internal .fig text path data was ignored" = "virheelliset sisäiset .fig-tekstipolun tiedot ohitettiin" +"invalid or oversized vector networks import as outlines" = "virheelliset tai liian suuret vektoriverkot tuodaan ääriviivoina" +"invalid or oversized vector path data was skipped" = "virheelliset tai liian suuret vektoripolun tiedot ohitettiin" +"just now" = "juuri nyt" +"key triggers without a supported key were skipped" = "näppäinlaukaisimet, joilla ei ole tuettua näppäintä, ohitettiin" +"key up triggers import as key presses" = "näppäimen vapautuslaukaisimet tuodaan näppäimen painalluksina" +"layers without a position were placed at their parent's origin" = "tasot, joilta puuttuu sijainti, sijoitettiin emotasonsa origoon" +"layers without a type were skipped" = "tasot, joilta puuttuu tyyppi, ohitettiin" +"legacy image filter container is unsupported; filter conversion was skipped" = "vanhaa kuvasuodinsäilöä ei tueta; suotimien muunnos ohitettiin" +"locked" = "lukittu" +"malformed image filters container was skipped" = "virheellinen image filters -säilö ohitettiin" +"malformed image paintFilter container was skipped" = "virheellinen image paintFilter -säilö ohitettiin" +"malformed slot content assignment was skipped" = "virheellinen sisältöpaikan sisällön määritys ohitettiin" +"no bleed" = "ei leikkuuvaraa" +"nobody else here yet" = "kukaan muu ei ole vielä paikalla" +"non-default slot settings are not represented and were skipped" = "oletuksesta poikkeavia sisältöpaikan asetuksia ei esitetä, ja ne ohitettiin" +"now" = "nyt" +"only in a text style or variable" = "vain tekstityylissä tai muuttujassa" +"or" = "tai" +"point stroke joins were preserved; dashed or variable-width strokes render with the layer join" = "pisteiden viivaliitokset säilytettiin; katko- ja vaihtelevanlevyiset viivat käyttävät tason liitostyyliä" +"preferred slot component keys cannot be resolved to file nodes and were skipped" = "sisältöpaikan ensisijaisten komponenttien avaimia ei voitu yhdistää tiedoston solmuihin, ja ne ohitettiin" +"prototype actions of unknown types were skipped" = "tuntemattoman tyyppiset prototyyppitoiminnot ohitettiin" +"prototype actions without a destination in the file were skipped" = "prototyyppitoiminnot, joiden kohdetta ei ole tiedostossa, ohitettiin" +"prototype expressions on variables that aren't in the variables JSON were skipped" = "muuttujiin viittaavat prototyyppilausekkeet ohitettiin, koska muuttujia ei ole muuttujien JSON-tiedostossa" +"prototype expressions with unsupported functions were skipped" = "prototyyppilausekkeet, joissa on tukemattomia funktioita, ohitettiin" +"prototype interactions without supported actions were skipped" = "prototyypin vuorovaikutukset, joissa ei ole tuettuja toimintoja, ohitettiin" +"prototype navigations of unknown types were skipped" = "tuntemattoman tyyppiset prototyypin siirtymät kohteeseen ohitettiin" +"prototype transitions of unknown types play instantly" = "tuntemattoman tyyppiset prototyypin siirtymäanimaatiot toistuvat välittömästi" +"prototype triggers of unknown types were skipped" = "tuntemattoman tyyppiset prototyypin laukaisimet ohitettiin" +"put a widget on the canvas" = "lisätä pienoisohjelman piirtoalueelle" +"read and edit the document" = "lukea ja muokata asiakirjaa" +"read the document" = "lukea asiakirjan" +"reads the document" = "lukee asiakirjan" +"set-variable actions on variables that aren't in the variables JSON were skipped" = "muuttujan asetustoiminnot ohitettiin, koska muuttujia ei ole muuttujien JSON-tiedostossa" +"set-variable actions with values that can't be evaluated were skipped" = "muuttujan asetustoiminnot ohitettiin, koska arvoja ei voi laskea" +"set-variable actions with values that don't fit the variable were skipped" = "muuttujan asetustoiminnot ohitettiin, koska arvot eivät sovi muuttujaan" +"set-variable actions without a value were skipped" = "muuttujan asetustoiminnot, joilta puuttuu arvo, ohitettiin" +"set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" = "muuttujatilan asetustoiminnot ohitettiin, koska kokoelmia tai tiloja ei ole muuttujien JSON-tiedostossa" +"slot content assignment target is missing or is not slot content" = "sisältöpaikan määrityksen kohde puuttuu tai ei ole sisältöpaikan sisältöä" +"slot content assignment uses the empty GUID sentinel and was skipped" = "sisältöpaikan määritys käyttää tyhjää GUID-erikoisarvoa, ja se ohitettiin" +"some vector points refer to point settings missing from the file; they use the layer's joins and plain corners" = "jotkin vektoripisteet viittaavat tiedostosta puuttuviin pisteasetuksiin; ne käyttävät tason liitostyyliä ja tavallisia kulmia" +"some vector segments refer to style settings missing from the file; they use the layer's stroke" = "jotkin vektorisegmentit viittaavat tiedostosta puuttuviin tyyliasetuksiin; ne käyttävät tason reunaviivaa" +"stale or invalid grid track sizing entries were ignored" = "vanhentuneet tai virheelliset ruudukon kokomääritykset ohitettiin" +"stars and polygons without point counts or geometry use the default point count" = "tähdet ja monikulmiot, joilta puuttuu pistemäärä tai geometria, käyttävät oletuspistemäärää" +"style settings of some vector segments aren't supported; those segments use the layer's stroke" = "joidenkin vektorisegmenttien tyyliasetuksia ei tueta; nämä segmentit käyttävät tason reunaviivaa" +"text path underline or strikethrough decoration is not rendered" = "tekstipolun alleviivausta tai yliviivausta ei renderöidä" +"text style overrides refer to missing table entries" = "tekstityylin ohitukset viittaavat puuttuviin taulukkokohteisiin" +"the file has no pages" = "tiedostossa ei ole sivuja" +"the plugin" = "liitännäinen" +"the variables JSON has no `variableCollections` (expected the local variables response) and was ignored" = "muuttujien JSON-tiedostosta puuttuu `variableCollections` (odotettiin paikallisten muuttujien vastausta), ja tiedosto ohitettiin" +"this session only" = "vain tämä istunto" +"unknown component property types were skipped" = "tuntemattoman tyyppiset komponenttiominaisuudet ohitettiin" +"variable aliases to variables that aren't in the variables JSON (or of another type) were skipped" = "muuttuja-aliakset ohitettiin, koska kohdemuuttujia ei ole muuttujien JSON-tiedostossa tai ne ovat eri tyyppiä" +"variable bindings to properties the layer doesn't have were skipped" = "muuttujasidokset ominaisuuksiin, joita tasolla ei ole, ohitettiin" +"variable bindings to variables that aren't in the variables JSON (library variables) were skipped" = "muuttujasidokset muuttujiin, joita ei ole muuttujien JSON-tiedostossa (kirjastomuuttujat), ohitettiin" +"variable bindings were skipped: the file JSON has no variable definitions (pass the local variables JSON as `variables`, or embed its `meta` next to the file JSON)" = "muuttujasidokset ohitettiin: tiedoston JSON ei määritä muuttujia (anna paikallisten muuttujien JSON arvona `variables` tai sisällytä sen `meta` tiedoston JSONin viereen)" +"variable bindings whose variable type doesn't fit the property were skipped" = "muuttujasidokset, joiden muuttujatyyppi ei sovi ominaisuuteen, ohitettiin" +"variable collections without modes were skipped" = "muuttujakokoelmat, joilla ei ole tiloja, ohitettiin" +"variable mode lookups in prototype expressions that aren't a variable and one of its modes were skipped" = "prototyyppilausekkeiden muuttujatilahaut, jotka eivät ole muuttuja ja jokin sen tiloista, ohitettiin" +"variable modes of collections that aren't in the file were skipped" = "tiedostosta puuttuvien kokoelmien muuttujatilat ohitettiin" +"variable modes of collections that aren't in the variables JSON were skipped" = "muuttujien JSON-tiedostosta puuttuvien kokoelmien muuttujatilat ohitettiin" +"variable values that don't match their type (or composed colours) were skipped" = "muuttujien arvot, jotka eivät vastaa tyyppiään (tai yhdistettyjä värejä), ohitettiin" +"variables of unknown types were skipped" = "tuntemattoman tyyppiset muuttujat ohitettiin" +"variables whose collection isn't in the variables JSON were skipped" = "muuttujat, joiden kokoelmaa ei ole muuttujien JSON-tiedostossa, ohitettiin" +"variables without readable values were skipped" = "muuttujat, joiden arvoja ei voi lukea, ohitettiin" +"vector points with unknown stroke joins use the layer join" = "vektoripisteet, joilla on tuntematon liitostyyli, käyttävät tason liitostyyliä" +"vectors without geometry are empty" = "vektorit ilman geometriaa ovat tyhjiä" +"vectors without geometry are empty (export the file JSON with geometry=paths)" = "vektorit ilman geometriaa ovat tyhjiä (vie tiedoston JSON asetuksella geometry=paths)" +"very long text was cut to 1,048,576 characters" = "erittäin pitkä teksti katkaistiin 1 048 576 merkkiin" +"very long vector paths were skipped" = "erittäin pitkät vektoripolut ohitettiin" +"video actions are not supported and were skipped" = "videotoimintoja ei tueta, ja ne ohitettiin" +"video triggers are not supported and were skipped" = "videolaukaisimia ei tueta, ja ne ohitettiin" +"yesterday" = "eilen" +"{instances} in {files}" = "{instances} tiedostoissa {files}" +"{language}: draft translation, waiting for a native speaker's review." = "{language}: käännösluonnos, joka odottaa äidinkielisen puhujan tarkistusta." +"{length} bleed" = "Leikkuuvara {length}" +"{month} {day}, {year}" = "{day}. {month} {year}" +"{name}\nClick to apply to the selection" = "{name}\nKäytä valintaan napsauttamalla" +"{name}\nDrag onto the canvas, or click to place in the middle of the view.\nHold Alt (Option) while dropping onto an instance to swap it." = "{name}\nVedä piirtoalueelle tai sijoita näkymän keskelle napsauttamalla.\nVaihda ilmentymä pudottamalla sen päälle Alt (Option) painettuna." +"{name} (from {library})" = "{name} (kirjastosta {library})" +"{name} (you)" = "{name} (sinä)" +"{name} ({layers} layers)" = "{name} (tasoja: {layers})" +"{name} ({tag}): {min} – {max}, default {default}. Double-click the slider to go back to the style's value; Escape while dragging cancels." = "{name} ({tag}): {min} – {max}, oletus {default}. Palauta tyylin arvo kaksoisnapsauttamalla liukusäädintä. Escape peruuttaa vetämisen." +"{name} cleared from the list" = "{name} poistettu luettelosta" +"{name} from {path}" = "{name} polusta {path}" +"{name} is forgotten on this computer; the file stays on {server}" = "{name} on unohdettu tällä tietokoneella. Tiedosto jää palvelimelle {server}." +"{name} is on the server." = "{name} on palvelimella." +"{name} is spotlighting" = "{name} nostaa näkymäänsä esiin" +"{name} was removed from the server" = "{name} poistettiin palvelimelta" +"{name} · deleted on main" = "{name} · poistettu päähaarassa" +"{name} · deleted on the branch" = "{name} · poistettu haarassa" +"{name} · not published" = "{name} · ei julkaistu" +"{name} · on a server" = "{name} · palvelimella" +"{name} · published version {v}" = "{name} · julkaistu versio {v}" +"{name} · version {v} on the server" = "{name} · versio {v} palvelimella" +"{name}, in a live session" = "{name}, reaaliaikaisessa istunnossa" +"{name}, online, open only to you" = "{name}, verkossa, vain sinun käytössäsi" +"{name}, {kind}, {w} by {h}, selected" = "{name}, {kind}, {w} × {h}, valittu" +"{n} added" = { one = "Lisätty: {n}", other = "Lisätty: {n}" } +"{n} autosave versions" = { one = "{n} automaattisesti tallennettu versio", other = "{n} automaattisesti tallennettua versiota" } +"{n} changed" = { one = "Muutettu: {n}", other = "Muutettu: {n}" } +"{n} columns" = { one = "{n} sarake", other = "{n} saraketta" } +"{n} components" = { one = "{n} komponentti", other = "{n} komponenttia" } +"{n} components and styles have newer versions" = { one = "Uudempi versio saatavilla: {n} komponentti tai tyyli", other = "Uudempia versioita saatavilla: {n} komponenttia ja tyyliä" } +"{n} conflicts to resolve" = { one = "{n} ratkaistava ristiriita", other = "{n} ratkaistavaa ristiriitaa" } +"{n} days ago" = { one = "{n} päivä sitten", other = "{n} päivää sitten" } +"{n} files" = { one = "{n} tiedosto", other = "{n} tiedostoa" } +"{n} hours ago" = { one = "{n} tunti sitten", other = "{n} tuntia sitten" } +"{n} instances" = { one = "{n} ilmentymä", other = "{n} ilmentymää" } +"{n} layers" = { one = "{n} taso", other = "{n} tasoa" } +"{n} layers as one file" = { one = "{n} taso yhtenä tiedostona", other = "{n} tasoa yhtenä tiedostona" } +"{n} layers selected" = { one = "{n} taso valittu", other = "{n} tasoa valittu" } +"{n} layers · {parts}" = { one = "{n} taso · {parts}", other = "{n} tasoa · {parts}" } +"{n} matches" = { one = "{n} osuma", other = "{n} osumaa" } +"{n} minutes ago" = { one = "{n} minuutti sitten", other = "{n} minuuttia sitten" } +"{n} months ago" = { one = "{n} kuukausi sitten", other = "{n} kuukautta sitten" } +"{n} more notes omitted from this report" = { one = "Tästä raportista on jätetty pois vielä {n} huomautus", other = "Tästä raportista on jätetty pois vielä {n} huomautusta" } +"{n} of {total}" = "{n} / {total}" +"{n} people joined" = { one = "{n} henkilö liittyi", other = "{n} henkilöä liittyi" } +"{n} preferred components" = { one = "{n} ensisijainen komponentti", other = "{n} ensisijaista komponenttia" } +"{n} ready" = { one = "{n} valmis", other = "{n} valmista" } +"{n} removed" = { one = "Poistettu: {n}", other = "Poistettu: {n}" } +"{n} replies" = { one = "{n} vastaus", other = "{n} vastausta" } +"{n} rows" = { one = "{n} rivi", other = "{n} riviä" } +"{n} styles" = { one = "{n} tyyli", other = "{n} tyyliä" } +"{n} text layers" = { one = "{n} tekstitaso", other = "{n} tekstitasoa" } +"{n} variables" = { one = "{n} muuttuja", other = "{n} muuttujaa" } +"{n} weeks ago" = { one = "{n} viikko sitten", other = "{n} viikkoa sitten" } +"{n} years ago" = { one = "{n} vuosi sitten", other = "{n} vuotta sitten" } +"{n}d" = "{n}pv" +"{n}h" = "{n}t" +"{n}m" = "{n}min" +"{n}w" = "{n}vk" +"{page}, nothing selected" = "{page}, ei valintaa" +"{profile} (current)" = "{profile} (nykyinen)" +"{property} is its value; {property ? \"a\" : \"b\"} picks a string. Other {…} stays as written." = "{property} on sen arvo. {property ? \"a\" : \"b\"} valitsee merkkijonon. Muut {…}-ilmaukset säilyvät sellaisinaan." +"{server} can't say what became of your files (it is older than this app)." = "{server} ei pysty kertomaan tiedostojesi tilaa (se on tätä sovellusta vanhempi)." +"{server} · No longer on the server" = "{server} · Ei enää palvelimella" +"{server} · {shared} · Opened {date}" = "{server} · {shared} · Avattu {date}" +"{tool} hold {key}" = "{tool} pidä {key} painettuna" +"{value} isn't a colour" = "{value} ei ole väri" +"{when} · edited" = "{when} · muokattu" +"←" = "←" +"← ↑ → ↓" = "← ↑ → ↓" +"← → ↑ ↓" = "← → ↑ ↓" +"→, Space or N" = "→, Space or N" +"↵" = "↵" +"⇥ / ⇧⇥" = "⇥ / ⇧⇥" +"⌘ arrows" = "⌘ arrows" +"⌘ click" = "⌘ click" +"⌘ scroll" = "⌘ scroll" +"⌥ drag" = "⌥ drag" + +[commands] +"align.bottom" = "Tasaa alas" +"align.hcenter" = "Keskitä vaakasuunnassa" +"align.left" = "Tasaa vasemmalle" +"align.right" = "Tasaa oikealle" +"align.top" = "Tasaa ylös" +"align.vcenter" = "Keskitä pystysuunnassa" +"annotation.add" = "Lisää huomautus" +"annotation.addCategory" = "Lisää huomautusluokka" +"annotation.categories" = "Huomautusten luokat" +"annotation.edit" = "Muokkaa huomautusta" +"annotation.editCategory" = "Muokkaa huomautusluokkaa" +"annotation.list" = "Luettele huomautukset" +"annotation.remove" = "Poista huomautus" +"annotation.removeCategory" = "Poista huomautusluokka" +"app.actions" = "Toiminnot" +"app.preferences" = "Asetukset" +"arrange.back" = "Vie takimmaiseksi" +"arrange.backward" = "Vie taaksepäin" +"arrange.forward" = "Tuo eteenpäin" +"arrange.front" = "Tuo etummaiseksi" +"arrange.packH" = "Pakkaa vaakasuunnassa" +"arrange.packV" = "Pakkaa pystysuunnassa" +"arrange.roundToPixels" = "Pyöristä pikseleihin" +"arrange.spacing" = "Välistys" +"arrange.tidy" = "Järjestä tasavälein" +"bool.exclude" = "Sulje valinnan päällekkäinen alue pois" +"bool.flatten" = "Yhdistä Boolen muoto vektoriksi" +"bool.intersect" = "Leikkaa valinnan yhteinen alue" +"bool.subtract" = "Vähennä valinta" +"bool.union" = "Yhdistä valinta" +"branch.archive" = "Arkistoi haara" +"branch.create" = "Luo haara" +"branch.list" = "Luettele haarat" +"branch.merge" = "Yhdistä haara" +"branch.open" = "Avaa haara" +"branch.review" = "Tarkista haara" +"branch.updateFromMain" = "Päivitä päähaarasta" +"codeConnect.export" = "Vie koodiyhteydet" +"codeConnect.import" = "Tuo koodiyhteydet" +"codeConnect.remove" = "Katkaise yhteys koodiin" +"codeConnect.set" = "Yhdistä koodiin" +"collab.cursor" = "Näytä kohdistin kohdassa" +"collab.cursorChat" = "Kohdistinkeskustelu" +"collab.diff" = "Näytä synkronoinnin muutokset" +"collab.files" = "Omat verkkotiedostot" +"collab.follow" = "Seuraa" +"collab.forgetFile" = "Unohda verkkotiedosto" +"collab.host" = "Aloita reaaliaikainen istunto" +"collab.join" = "Liity reaaliaikaiseen istuntoon" +"collab.leave" = "Poistu reaaliaikaisesta istunnosta" +"collab.manageFile" = "Hallitse julkaistua tiedostoa" +"collab.openFile" = "Avaa verkkotiedosto" +"collab.peers" = "Tämän tiedoston käyttäjät" +"collab.publish" = "Julkaise palvelimelle" +"collab.recoverFile" = "Palauta verkkotiedosto" +"collab.registerLinks" = "Avaa kutsulinkit FigCraftissa" +"collab.removePerson" = "Poista istunnosta" +"collab.report" = "Ilmoita tiedostosta palvelimen ylläpitäjälle" +"collab.serverInfo" = "Palvelimen tiedot" +"collab.share" = "Jaa liittymiskoodilla" +"collab.spotlight" = "Nosta minut esiin" +"collab.stop" = "Lopeta reaaliaikainen istunto" +"command.batch" = "Suorita useita komentoja" +"comment.add" = "Lisää kommentti" +"comment.delete" = "Poista kommentti" +"comment.edit" = "Muokkaa kommenttia" +"comment.link" = "Kopioi linkki kommenttiin" +"comment.list" = "Luettele kommentit" +"comment.markRead" = "Merkitse kommentti luetuksi" +"comment.move" = "Siirrä kommentti" +"comment.next" = "Seuraava kommentti" +"comment.open" = "Avaa kommentti" +"comment.people" = "Kommenttien osallistujat" +"comment.react" = "Reagoi kommenttiin" +"comment.reply" = "Vastaa kommenttiin" +"comment.resolve" = "Ratkaise kommentti" +"comment.zoom" = "Zoomaa kommentteihin" +"component.addProperty" = "Lisää komponentin ominaisuus" +"component.addVariant" = "Lisää variantti" +"component.bindProperty" = "Sido taso ominaisuuteen" +"component.combineVariants" = "Yhdistä varianteiksi" +"component.create" = "Luo komponentti" +"component.createMultiple" = "Luo useita komponentteja" +"component.deleteProperty" = "Poista komponentin ominaisuus" +"component.detach" = "Irrota ilmentymä" +"component.editProperty" = "Muokkaa komponentin ominaisuutta" +"component.exposeInstance" = "Tuo sisäkkäinen ilmentymä käyttöön" +"component.goToMain" = "Siirry pääkomponenttiin" +"component.hideWhenPublishing" = "Kätke julkaistaessa" +"component.list" = "Luettele komponentit" +"component.makeSlot" = "Luo sisältöpaikka" +"component.playground" = "Komponenttien kokeilualue" +"component.properties" = "Komponentin ominaisuudet" +"component.pushChanges" = "Siirrä muutokset pääkomponenttiin" +"component.removeSlot" = "Poista sisältöpaikka" +"component.resetInstance" = "Palauta kaikki muutokset" +"component.resetOverride" = "Palauta ohitus" +"component.restoreMain" = "Palauta pääkomponentti" +"dev.compare" = "Vertaa muutoksia" +"dev.focus" = "Kohdistusnäkymä" +"dev.resolveChange" = "Ratkaise muutos" +"dev.unfocus" = "Poistu kohdistusnäkymästä" +"devResource.add" = "Lisää kehittäjäresurssi" +"devResource.edit" = "Muokkaa kehittäjäresurssia" +"devResource.list" = "Luettele kehittäjäresurssit" +"devResource.remove" = "Poista kehittäjäresurssi" +"distribute.horizontal" = "Jaa vaakavälit tasan" +"distribute.vertical" = "Jaa pystyvälit tasan" +"doc.home" = "Aloitus" +"doc.list" = "Luettele asiakirjat" +"doc.nextTab" = "Seuraava välilehti" +"doc.previousTab" = "Edellinen välilehti" +"doc.switch" = "Vaihda asiakirjaa" +"edit.copy" = "Kopioi" +"edit.copyAsCss" = "Kopioi CSS-muodossa" +"edit.copyAsPng" = "Kopioi PNG-muodossa" +"edit.copyAsSvg" = "Kopioi SVG-muodossa" +"edit.copyAsText" = "Kopioi tekstinä" +"edit.copyLink" = "Kopioi linkki valintaan" +"edit.copyProperties" = "Kopioi ominaisuudet" +"edit.cut" = "Leikkaa" +"edit.delete" = "Poista" +"edit.duplicate" = "Monista" +"edit.findReplace" = "Etsi ja korvaa" +"edit.paste" = "Liitä" +"edit.pasteDesignLayers" = "Liitä suunnittelutasoja" +"edit.pasteOver" = "Liitä valinnan päälle" +"edit.pasteProperties" = "Liitä ominaisuudet" +"edit.pasteReplace" = "Liitä korvaten" +"edit.pickColor" = "Poimi väri" +"edit.redo" = "Tee uudelleen" +"edit.repeat" = "Toista viimeisin komento" +"edit.setDefaultProperties" = "Aseta oletusominaisuudet" +"edit.undo" = "Kumoa" +"effect.add" = "Lisää tehoste" +"effect.move" = "Siirrä tehoste" +"effect.remove" = "Poista tehoste" +"effect.set" = "Aseta tehoste" +"export.add" = "Lisää vientiasetus" +"export.remove" = "Poista vientiasetus" +"export.update" = "Muokkaa vientiasetusta" +"file.close" = "Sulje välilehti" +"file.colorProfile" = "Väriprofiili" +"file.duplicate" = "Monista tiedosto" +"file.export" = "Vie" +"file.exportFramesPdf" = "Vie kehykset PDF-muodossa" +"file.exportLayers" = "Vie tasot" +"file.exportSvg" = "Vie valinta SVG-muodossa" +"file.import" = "Tuo" +"file.new" = "Uusi suunnittelutiedosto" +"file.newFromTemplate" = "Uusi mallista" +"file.open" = "Avaa" +"file.openRecent" = "Avaa viimeksi avattu" +"file.placeImage" = "Sijoita kuva" +"file.recover" = "Palauta tiedosto" +"file.rename" = "Nimeä tiedosto uudelleen" +"file.revert" = "Palauta tallennettu" +"file.save" = "Tallenna" +"file.saveAs" = "Tallenna nimellä" +"file.saveVersion" = "Tallenna versiohistoriaan" +"file.versionHistory" = "Näytä versiohistoria" +"fill.add" = "Lisää täyttö" +"fill.move" = "Siirrä täyttö" +"fill.pattern" = "Kuviotäyttö" +"fill.remove" = "Poista täyttö" +"fill.set" = "Aseta täyttö" +"frame.create" = "Luo kehys" +"frame.presets" = "Luettele kehysten esiasetukset" +"frame.swapOrientation" = "Vaihda suunta" +"grid.add" = "Lisää asetteluruudukko" +"grid.move" = "Siirrä asetteluruudukko" +"grid.remove" = "Poista asetteluruudukko" +"guide.add" = "Lisää apuviiva" +"guide.clear" = "Poista kaikki apuviivat" +"guide.move" = "Siirrä apuviiva" +"guide.remove" = "Poista apuviiva" +"guide.select" = "Valitse apuviiva" +"help.about" = "Tietoja FigCraftista" +"help.gettingStarted" = "Aloittaminen" +"help.page" = "Ohjesivu" +"help.shortcuts" = "Pikanäppäimet" +"image.adjust" = "Säädä kuvaa" +"image.deletePreset" = "Poista kuvan esiasetus" +"image.place" = "Sijoita kuva" +"image.presets" = "Kuvan esiasetukset" +"image.savePreset" = "Tallenna kuvan esiasetus" +"inspect.assets" = "Tarkastele resursseja" +"inspect.code" = "Tason koodi" +"inspect.componentCode" = "Komponentin koodi" +"inspect.properties" = "Tarkastele ominaisuuksia" +"inspect.readyForDev" = "Kehitysvalmis" +"instance.create" = "Luo ilmentymä" +"instance.exposed" = "Käyttöön tuodut sisäkkäiset ilmentymät" +"instance.place" = "Sijoita ilmentymä" +"instance.resetSlot" = "Palauta sisältöpaikka" +"instance.setProperties" = "Aseta ilmentymän ominaisuudet" +"instance.slotInsert" = "Lisää sisältöpaikkaan" +"instance.swap" = "Vaihda ilmentymä" +"instance.swapAt" = "Vaihda ilmentymä pisteessä" +"layers.collapseAll" = "Supista tasot" +"layout.add" = "Lisää automaattinen asettelu" +"layout.remove" = "Poista automaattinen asettelu" +"layout.setTrack" = "Aseta ruudukon osan koko" +"layout.suggest" = "Ehdota automaattista asettelua" +"libraries.open" = "Kirjastot" +"libraries.publish" = "Julkaise kirjasto…" +"library.acceptUpdates" = "Hyväksy kirjaston päivitykset" +"library.add" = "Lisää kirjasto" +"library.aliasVariable" = "Luo alias kirjastomuuttujaan" +"library.applyStyle" = "Käytä kirjastotyyliä" +"library.bindVariable" = "Sido kirjastomuuttuja" +"library.extendCollection" = "Laajenna kirjastokokoelmaa" +"library.import" = "Tuo kirjastokomponentti" +"library.importVariable" = "Tuo kirjastomuuttuja" +"library.list" = "Luettele kirjastot" +"library.place" = "Sijoita kirjastokomponentti" +"library.publish" = "Julkaise kirjasto" +"library.publishToServer" = "Julkaise kirjasto palvelimelle" +"library.remove" = "Poista kirjasto" +"library.setVariableMode" = "Aseta kirjastomuuttujan tila" +"library.swap" = "Vaihda kirjasto" +"library.swapAt" = "Vaihda ilmentymä kirjastokomponenttiin" +"library.updates" = "Kirjaston päivitykset" +"library.usage" = "Kirjaston käyttö" +"measure.distances" = "Mittaa etäisyyksiä" +"measure.padding" = "Mittaa automaattisen asettelun sisämarginaali" +"node.create" = "Luo taso" +"node.get" = "Hae taso" +"node.reorder" = "Muuta tason järjestystä" +"node.reparent" = "Siirrä taso kohteeseen" +"node.set" = "Aseta ominaisuudet" +"object.batchRename" = "Nimeä joukko uudelleen" +"object.convertToFrame" = "Muunna kehykseksi" +"object.convertToGroup" = "Muunna ryhmäksi" +"object.convertToSection" = "Muunna osioksi" +"object.flipH" = "Peilaa vaakasuunnassa" +"object.flipV" = "Peilaa pystysuunnassa" +"object.frameSelection" = "Kehystä valinta" +"object.group" = "Ryhmittele valinta" +"object.hideOthers" = "Kätke muut tasot" +"object.mask" = "Käytä maskina" +"object.moveToPage" = "Siirrä sivulle" +"object.opacityKeys" = "Peittävyys numeronäppäimillä" +"object.rasterize" = "Rasteroi valinta" +"object.releaseMask" = "Vapauta maski" +"object.removeFill" = "Poista täyttö" +"object.removeFrame" = "Poista kehys" +"object.removeStroke" = "Poista reunaviiva" +"object.rename" = "Nimeä uudelleen" +"object.resizeToFit" = "Sovita koko" +"object.rotate180" = "Kierrä 180°" +"object.rotateLeft" = "Kierrä 90° vasemmalle" +"object.rotateRight" = "Kierrä 90° oikealle" +"object.setThumbnail" = "Aseta pikkukuvaksi" +"object.swapFillStroke" = "Vaihda täyttö ja reunaviiva keskenään" +"object.toggleLock" = "Lukitse/avaa valinnan lukitus" +"object.toggleVisible" = "Näytä/kätke valinta" +"object.ungroup" = "Pura valinnan ryhmittely" +"object.wrapInSection" = "Ympäröi uudella osiolla" +"page.add" = "Lisää sivu" +"page.delete" = "Poista sivu" +"page.duplicate" = "Monista sivu" +"page.list" = "Luettele sivut" +"page.move" = "Siirrä sivu" +"page.rename" = "Nimeä sivu uudelleen" +"page.select" = "Siirry sivulle" +"page.setBackground" = "Sivun tausta" +"paint.documentColors" = "Asiakirjan värit" +"plugin.close" = "Sulje liitännäispaneeli" +"plugin.directory.install" = "Asenna liitännäinen hakemistosta" +"plugin.directory.list" = "Selaa liitännäishakemistoa" +"plugin.directory.publish" = "Julkaise liitännäinen hakemistoon" +"plugin.directory.unpublish" = "Peru liitännäisen julkaisu hakemistosta" +"plugin.event" = "Käytä liitännäispaneelin säädintä" +"plugin.import" = "Tuo liitännäinen manifestista…" +"plugin.list" = "Luettele liitännäiset" +"plugin.remove" = "Poista liitännäinen" +"plugin.run" = "Suorita liitännäinen" +"plugin.ui" = "Liitännäispaneeli" +"plugins.manage" = "Hallitse liitännäisiä…" +"plugins.runLast" = "Suorita viimeisin liitännäinen" +"pref.autoSave" = "Tallenna tiedostot automaattisesti" +"pref.highlightHover" = "Korosta tasot osoitettaessa" +"pref.keepTool" = "Pidä työkalu valittuna käytön jälkeen" +"pref.language" = "Käyttöliittymän kieli" +"pref.nudge" = "Siirtoaskeleen koko" +"pref.numberKeysOpacity" = "Säädä peittävyyttä numeronäppäimillä" +"pref.publishServer" = "Verkkojakamisen palvelin" +"pref.reduceMotion" = "Vähennä liikettä" +"pref.renameDuplicates" = "Nimeä monistetut tasot uudelleen" +"pref.showDimensions" = "Näytä objektien mitat" +"pref.userName" = "Nimesi kommenteissa" +"proto.action.add" = "Lisää prototyyppitoiminto" +"proto.action.move" = "Muuta prototyyppitoimintojen järjestystä" +"proto.action.remove" = "Poista prototyyppitoiminto" +"proto.action.set" = "Muokkaa prototyyppitoimintoa" +"proto.closePreview" = "Sulje prototyypin esikatselu" +"proto.conditional.addBlock" = "Lisää muuten jos / muuten" +"proto.conditional.removeBlock" = "Poista jos / muuten jos / muuten" +"proto.conditional.setCondition" = "Muokkaa ehtoa" +"proto.evaluate" = "Laske prototyyppilauseke" +"proto.flow.add" = "Lisää kulun aloituspiste" +"proto.flow.remove" = "Poista kulku" +"proto.flow.rename" = "Nimeä kulku uudelleen" +"proto.interaction.add" = "Lisää vuorovaikutus" +"proto.interaction.clear" = "Poista kaikki vuorovaikutukset" +"proto.interaction.remove" = "Poista vuorovaikutus" +"proto.interaction.set" = "Muokkaa vuorovaikutusta" +"proto.present" = "Esitä" +"proto.presentOptions" = "Esityksen asetukset" +"proto.preview" = "Avaa prototyypin esikatselu" +"proto.settings" = "Prototyypin asetukset" +"section.toggleContents" = "Kätke/näytä osion sisältö" +"select.add" = "Lisää valintaan" +"select.all" = "Valitse kaikki" +"select.area" = "Valitse alue" +"select.byName" = "Valitse tasoja nimen perusteella" +"select.children" = "Valitse alikohteet" +"select.inverse" = "Käännä valinta" +"select.layersAt" = "Pisteessä olevat tasot" +"select.matching" = "Valitse vastaavat tasot" +"select.next" = "Valitse seuraava rinnakkaiskohde" +"select.none" = "Poista valinta" +"select.parent" = "Valitse emotaso" +"select.previous" = "Valitse edellinen rinnakkaiskohde" +"select.remove" = "Poista valinnasta" +"select.sameEffect" = "Valitse sama tehoste" +"select.sameFill" = "Valitse sama täyttö" +"select.sameFont" = "Valitse sama fontti" +"select.sameInstance" = "Valitse sama ilmentymä" +"select.sameProperties" = "Valitse samat ominaisuudet" +"select.sameStroke" = "Valitse sama reunaviiva" +"select.sameText" = "Valitse samat tekstiominaisuudet" +"select.set" = "Valitse" +"selection.applyColorStyle" = "Käytä valinnan värityyliä" +"selection.colors" = "Valinnan värit" +"selection.selectColor" = "Valitse tämänväriset tasot" +"selection.setPaint" = "Muuta valinnan väriä" +"shape.arrow" = "Nuoli" +"shape.ellipse" = "Ellipsi" +"shape.line" = "Viiva" +"shape.polygon" = "Monikulmio" +"shape.rectangle" = "Suorakulmio" +"shape.star" = "Tähti" +"shapeBuilder.merge" = "Yhdistä alueet" +"shapeBuilder.mode" = "Muodonrakentimen tila" +"shapeBuilder.regions" = "Muodonrakentimen alueet" +"shapeBuilder.remove" = "Poista alueet" +"stroke.add" = "Lisää reunaviiva" +"stroke.brush" = "Sivellin" +"stroke.move" = "Siirrä reunaviiva" +"stroke.remove" = "Poista reunaviiva" +"stroke.set" = "Aseta reunaviiva" +"stroke.widthProfile" = "Leveysprofiili" +"style.apply" = "Käytä tyyliä" +"style.bind" = "Sido muuttuja tyylissä" +"style.create" = "Luo tyyli" +"style.delete" = "Poista tyyli" +"style.detach" = "Irrota tyyli" +"style.duplicate" = "Monista tyyli" +"style.list" = "Luettele tyylit" +"style.move" = "Siirrä tyylejä" +"style.renameFolder" = "Nimeä tyylikansio uudelleen" +"style.update" = "Muokkaa tyyliä" +"template.list" = "Luettele mallit" +"text.alignCenter" = "Keskitä" +"text.alignJustify" = "Tasaa molemmat reunat" +"text.alignLeft" = "Tasaa vasemmalle" +"text.alignRight" = "Tasaa oikealle" +"text.bold" = "Lihavointi" +"text.bulletList" = "Luettelomerkitty luettelo" +"text.case" = "Kirjainkoko" +"text.caseLower" = "Pienet kirjaimet" +"text.caseOriginal" = "Alkuperäinen kirjainkoko" +"text.caseSmallCaps" = "Kapiteelit" +"text.caseTitle" = "Sanojen isot alkukirjaimet" +"text.caseUpper" = "Isot kirjaimet" +"text.create" = "Teksti" +"text.decoration" = "Koristelu" +"text.edit.begin" = "Muokkaa tekstiä" +"text.edit.caret" = "Aseta kohdistin" +"text.edit.end" = "Lopeta tekstin muokkaus" +"text.edit.insert" = "Kirjoita tekstiä" +"text.edit.key" = "Tekstinäppäin" +"text.edit.preedit" = "Koosta tekstiä (IME)" +"text.edit.selectLine" = "Valitse rivi" +"text.edit.selectWord" = "Valitse sana" +"text.edit.selection" = "Tekstivalinta" +"text.endPreview" = "Lopeta tekstin esikatselu" +"text.fonts" = "Luettele fontit" +"text.italic" = "Kursivointi" +"text.lineHeightDown" = "Pienennä rivikorkeutta" +"text.lineHeightUp" = "Suurenna rivikorkeutta" +"text.link" = "Luo linkki" +"text.listIndent" = "Sisennä luettelo" +"text.listOutdent" = "Vähennä luettelon sisennystä" +"text.listStyle" = "Luettelotyyli" +"text.maxLines" = "Rivien enimmäismäärä" +"text.missingFonts" = "Puuttuvat fontit" +"text.numberList" = "Numeroitu luettelo" +"text.onPath" = "Teksti polulla" +"text.paragraphIndent" = "Kappaleen sisennys" +"text.paragraphSpacing" = "Kappaleväli" +"text.pathOptions" = "Tekstipolun asetukset" +"text.preview" = "Esikatsele tekstimuutosta" +"text.removeFromPath" = "Poista teksti polulta" +"text.set" = "Aseta teksti" +"text.setFeatures" = "OpenType-ominaisuudet" +"text.setVariations" = "Muuttuvan fontin akselit" +"text.sizeDown" = "Pienennä fonttikokoa" +"text.sizeUp" = "Suurenna fonttikokoa" +"text.strikethrough" = "Yliviivaus" +"text.style" = "Muotoile tekstialue" +"text.trackingDown" = "Pienennä merkkiväliä" +"text.trackingUp" = "Suurenna merkkiväliä" +"text.underline" = "Alleviivaus" +"text.weightDown" = "Vähennä fontin paksuutta" +"text.weightUp" = "Lisää fontin paksuutta" +"tool.arrow" = "Nuoli" +"tool.comment" = "Kommentti" +"tool.ellipse" = "Ellipsi" +"tool.frame" = "Kehys" +"tool.hand" = "Käsi" +"tool.line" = "Viiva" +"tool.move" = "Siirrä" +"tool.pen" = "Kynä" +"tool.pencil" = "Lyijykynä" +"tool.pencilBrush" = "Lyijykynäsivellin" +"tool.place" = "Sijoita työkalulla" +"tool.polygon" = "Monikulmio" +"tool.rectangle" = "Suorakulmio" +"tool.scale" = "Skaalaus" +"tool.scaleAnchor" = "Skaalauksen ankkuri" +"tool.section" = "Osio" +"tool.set" = "Valitse työkalu" +"tool.shapeBuilder" = "Muodonrakennin" +"tool.slice" = "Viipale" +"tool.star" = "Tähti" +"tool.text" = "Teksti" +"transform.move" = "Siirrä" +"transform.moveTo" = "Siirrä kohteeseen" +"transform.nudge" = "Siirrä askelittain" +"transform.nudgeSize" = "Muuta kokoa askelittain" +"transform.resize" = "Muuta kokoa" +"transform.rotate" = "Kierrä" +"transform.rotationOrigin" = "Siirrä kiertokeskipistettä" +"transform.scale" = "Skaalaus" +"transform.setRotationOrigin" = "Aseta kiertokeskipiste" +"variable.addMode" = "Lisää tila" +"variable.bind" = "Sido muuttuja" +"variable.create" = "Luo muuttuja" +"variable.createCollection" = "Luo muuttujakokoelma" +"variable.delete" = "Poista muuttuja" +"variable.deleteCollection" = "Poista kokoelma" +"variable.duplicate" = "Monista muuttuja" +"variable.export" = "Vie muuttujat" +"variable.import" = "Tuo muuttujat" +"variable.list" = "Luettele muuttujat" +"variable.moveMode" = "Siirrä tila" +"variable.removeMode" = "Poista tila" +"variable.renameCollection" = "Nimeä kokoelma uudelleen" +"variable.renameMode" = "Nimeä tila uudelleen" +"variable.resetOverride" = "Palauta muuttujan ohitus" +"variable.set" = "Aseta muuttujan arvo" +"variable.setMode" = "Aseta muuttujan tila" +"variable.update" = "Muokkaa muuttujaa" +"vector.create" = "Vektori" +"vector.deleteHeal" = "Poista ja korjaa valinta" +"vector.edit.addVertex" = "Lisää piste" +"vector.edit.begin" = "Muokkaa vektoria" +"vector.edit.bend" = "Taivuta segmenttiä" +"vector.edit.bounds" = "Valittujen pisteiden rajat" +"vector.edit.connect" = "Yhdistä pisteet" +"vector.edit.cornerRadius" = "Pisteen kulmasäde" +"vector.edit.cut" = "Leikkaa polku" +"vector.edit.delete" = "Poista pisteet" +"vector.edit.end" = "Lopeta muokkaus" +"vector.edit.erase" = "Pyyhi" +"vector.edit.lasso" = "Valitse lassolla" +"vector.edit.mirroring" = "Kahvojen peilaus" +"vector.edit.moveHandles" = "Siirrä kahvoja" +"vector.edit.moveVertices" = "Siirrä pisteitä" +"vector.edit.nudge" = "Siirrä pisteitä askelittain" +"vector.edit.paint" = "Maalipurkki" +"vector.edit.pen" = "Kynän piste" +"vector.edit.penEnd" = "Päätä kynäpolku" +"vector.edit.position" = "Pisteen sijainti" +"vector.edit.resize" = "Muuta pisteiden kokoa" +"vector.edit.rotate" = "Kierrä pisteitä" +"vector.edit.select" = "Valitse pisteitä" +"vector.edit.selectAll" = "Valitse kaikki pisteet" +"vector.edit.setHandle" = "Aseta kahva" +"vector.edit.smooth" = "Pehmennä tai terävöitä pisteitä" +"vector.edit.state" = "Vektorimuokkauksen tila" +"vector.edit.strokeCap" = "Pisteen viivanpää" +"vector.edit.strokeJoin" = "Pisteen viivaliitos" +"vector.edit.tool" = "Vektorimuokkauksen työkalu" +"vector.edit.transform" = "Muunna pisteitä" +"vector.edit.widthPoint" = "Leveyspiste" +"vector.flatten" = "Yhdistä vektorimuodoksi" +"vector.join" = "Yhdistä valinta" +"vector.offset" = "Siirrä polun reunaa" +"vector.outlineStroke" = "Muunna reunaviiva poluksi" +"vector.reverse" = "Käännä polun suunta" +"vector.simplify" = "Yksinkertaista vektori" +"vector.smoothJoin" = "Yhdistä valinta pehmeästi" +"version.autosave" = "Tallenna versio automaattisesti" +"version.delete" = "Poista versio" +"version.duplicate" = "Monista versio" +"version.exitPreview" = "Takaisin nykyiseen versioon" +"version.list" = "Luettele versiot" +"version.preview" = "Katsele versiota" +"version.rename" = "Nimeä versio uudelleen" +"version.restore" = "Palauta versio" +"view.assets" = "Resurssipaneeli" +"view.comments" = "Kommentit" +"view.contextMenu" = "Näytä pikavalikko" +"view.design" = "Suunnittelupaneeli" +"view.designPrototype" = "Vaihda suunnittelun ja prototyypin välillä" +"view.devMode" = "Kehittäjätila" +"view.frameOutlines" = "Kehysten ääriviivat" +"view.fullscreen" = "Koko näyttö" +"view.get" = "Hae näkymä" +"view.layers" = "Tasopaneeli" +"view.layoutGrids" = "Asetteluruudukot" +"view.leftSidebar" = "Näytä/kätke vasen sivupalkki" +"view.maskOutlines" = "Maskien ääriviivat" +"view.multiplayerCursors" = "Yhteiskäytön kohdistimet" +"view.nextFrame" = "Zoomaa seuraavaan kehykseen" +"view.nextPage" = "Seuraava sivu" +"view.nextRegion" = "Kohdista seuraava alue" +"view.outlines" = "Ääriviivat" +"view.outlinesHiddenLayers" = "Sisällytä kätketyt tasot ääriviivoihin" +"view.outlinesObjectBounds" = "Sisällytä objektien rajat ääriviivoihin" +"view.outlinesSelection" = "Valinnan ääriviivat" +"view.pan" = "Siirrä näkymää" +"view.panelWidths" = "Palauta paneelien leveydet" +"view.pixelGrid" = "Pikseliruudukko" +"view.pixelPreview" = "Pikseliesikatselu" +"view.prevFrame" = "Zoomaa edelliseen kehykseen" +"view.prevPage" = "Edellinen sivu" +"view.previousRegion" = "Kohdista edellinen alue" +"view.proofSrgb" = "sRGB-vedossimulointi" +"view.prototype" = "Prototyyppipaneeli" +"view.rulers" = "Viivaimet" +"view.set" = "Aseta näkymä" +"view.slices" = "Viipaleet" +"view.snapGeometry" = "Tartu geometriaan" +"view.snapObjects" = "Tartu objekteihin" +"view.snapPixel" = "Tartu pikseliruudukkoon" +"view.themeDark" = "Tumma teema" +"view.themeLight" = "Vaalea teema" +"view.themeSystem" = "Seuraa järjestelmän teemaa" +"view.toggleUi" = "Näytä/kätke käyttöliittymä" +"view.zoom100" = "Zoomaa kokoon 100 %" +"view.zoom200" = "Zoomaa kokoon 200 %" +"view.zoom50" = "Zoomaa kokoon 50 %" +"view.zoomAt" = "Zoomaustyökalu" +"view.zoomFit" = "Sovita näkymään" +"view.zoomIn" = "Lähennä" +"view.zoomOut" = "Loitonna" +"view.zoomSelection" = "Zoomaa valintaan" +"widget.click" = "Napsauta pienoisohjelmaa" +"widget.insert" = "Lisää pienoisohjelma" +"widget.list" = "Luettele pienoisohjelmat" +"window.focus" = "Tuo ikkuna etummaiseksi" +"window.new" = "Uusi ikkuna" + +[menu] +"bool.exclude" = "Jätä pois" +"bool.flatten" = "Yhdistä vektorimuodoksi" +"bool.intersect" = "Leikkaus" +"bool.subtract" = "Vähennä" +"bool.union" = "Yhdiste" +"file.duplicate" = "Monista" +"file.import" = "Tuo tiedostosta…" +"file.rename" = "Nimeä uudelleen" +"libraries.open" = "Avaa kirjastot" +"select.sameEffect" = "Tehoste" +"select.sameFill" = "Täyttö" +"select.sameFont" = "Fontti" +"select.sameInstance" = "Ilmentymä" +"select.sameProperties" = "Ominaisuudet" +"select.sameStroke" = "Reunaviiva" +"select.sameText" = "Tekstiominaisuudet" +"text.alignCenter" = "Keskitä teksti" +"text.alignJustify" = "Tasaa tekstin molemmat reunat" +"text.alignLeft" = "Tasaa teksti vasemmalle" +"text.alignRight" = "Tasaa teksti oikealle" +"view.assets" = "Resurssit" +"view.design" = "Suunnittelu" +"view.layers" = "Tasot" +"view.outlines" = "Näytä ääriviivat" +"view.outlinesHiddenLayers" = "Sisällytä kätketyt tasot" +"view.outlinesObjectBounds" = "Sisällytä objektien rajat" +"view.outlinesSelection" = "Näytä valinnan ääriviivat" +"view.prototype" = "Prototyyppi" +"view.themeDark" = "Tumma" +"view.themeLight" = "Vaalea" +"view.themeSystem" = "Seuraa järjestelmää" diff --git a/crates/ui-egui/src/i18n_tests.rs b/crates/ui-egui/src/i18n_tests.rs index d4d81d7b..ea85ad32 100644 --- a/crates/ui-egui/src/i18n_tests.rs +++ b/crates/ui-egui/src/i18n_tests.rs @@ -8,6 +8,46 @@ use super::*; /// Languages that must translate every UI string and command label. const COMPLETE: &[&str] = &["es"]; +#[test] +fn finnish_locale_tags_and_integer_plurals() { + let fi = resolve("fi", None); + assert_eq!(LANGS[fi].code, "fi"); + assert_eq!(LANGS[fi].name, "Suomi"); + assert!(LANGS[fi].draft, "AI-assisted Finnish still needs human review"); + let mut session = figcraft_engine::Session::new(); + session.execute("pref.language", &serde_json::json!({"language":"fi"})).unwrap(); + let saved = serde_json::to_string(&session.prefs).unwrap(); + let restored: figcraft_engine::Prefs = serde_json::from_str(&saved).unwrap(); + assert_eq!(restored.language, "fi"); + for locale in ["fi-FI", "fi_FI.UTF-8", "FI-fi", "fi"] { + assert_eq!(resolve("system", Some(locale)), fi, "{locale}"); + } + for (count, expected) in [(0, "0 tasoa"), (1, "1 taso"), (2, "2 tasoa"), (21, "21 tasoa"), (101, "101 tasoa")] { + assert_eq!(fill(counted(fi, "{n} layers", count), &[("n", &count)]), expected); + } + assert_eq!(fill(counted(fi, "{n} minutes ago", 1), &[("n", &1)]), "1 minuutti sitten"); + assert_eq!(fill(counted(fi, "{n} minutes ago", 2), &[("n", &2)]), "2 minuuttia sitten"); + assert_eq!(fill(counted(fi, "{n} people joined", 2), &[("n", &2)]), "2 henkilöä liittyi"); +} + +#[test] +fn finnish_keeps_interpolated_names_and_context_specific_labels() { + let fi = resolve("fi", None); + let text = |key| match lookup(fi, key).unwrap() { + Text::Plain(s) => *s, + _ => panic!("expected a plain Finnish label: {key}"), + }; + assert_eq!(text("Cancel"), "Peruuta"); + assert_eq!(text("Fill"), "Täyttö"); + assert_eq!(text("Fill|sizing"), "Täytä"); + assert_eq!(text("Join|stroke"), "Liitos"); + assert_eq!(text("Open|comments"), "Avoin"); + assert_eq!(fill(text("Actions for {name}"), &[("name", &"File {1}.figcraft")]), "Kohteen File {1}.figcraft toiminnot"); + assert!(lookup(fi, "My layer {name}.svg").is_none()); + assert!(matches!(find(LANGS[fi].commands, "edit.undo"), Some(Text::Plain("Kumoa")))); + assert!(matches!(find(LANGS[fi].menu, "file.import"), Some(Text::Plain("Tuo tiedostosta…")))); +} + /// Keys looked up from data rather than written as `tr("…")` in the code: the main menu's /// submenu names (the engine's menu catalog) and labels of engine and document enums. fn dynamic_keys() -> BTreeSet { diff --git a/docs/translating.md b/docs/translating.md index 1ea1b94f..3700bab4 100644 --- a/docs/translating.md +++ b/docs/translating.md @@ -68,6 +68,18 @@ language also needs its script in `cjk::language`. ## Adding a language +Finnish (`fi`, Suomi) is an AI-assisted draft awaiting native-speaker review. Its initial catalog +covers the current 1,729 English UI keys, 567 command labels and 31 menu-specific wordings, including +42 plural entries. Counts use singular only for 1 (`1 taso`, `0/2/21 tasoa`); this already follows +the default integer rule. Context keys distinguish paint `Fill` (Täyttö), sizing `Fill|sizing` +(Täytä), stroke `Join|stroke` (Liitos) and comment status `Open|comments` (Avoin). +The build discovers `fi.toml` automatically; `fi-FI` and `fi_FI.UTF-8` resolve to it. Product names, +code identifiers and shortcut notation retain their identity. Like the other unreviewed catalogs, +Finnish remains a draft outside `COMPLETE`; future English additions may fall back until translated. +Noise modes Mono/Duo/Multi describe one, two or many grain colours and read Yksivärinen, +Kaksivärinen and Monivärinen, independently of their stable enum names. +The narrow glass-effect light-intensity field uses Vahvuus so its complete label fits beside the value. + 1. Copy `crates/ui-egui/locales/es.toml` to `.toml` (a BCP 47 code: `fr`, `pt-BR`, `ja`) and set `[meta] name` and `status = "draft — needs native review"`. 2. Translate every value; keep the keys. Keep `{placeholders}` exactly, "…" where English has it, From e35f8eb1b78f7f30355509144e4e4f0d4a2f1905 Mon Sep 17 00:00:00 2001 From: Ashsajal Date: Sun, 18 Oct 2026 14:15:56 +0600 Subject: [PATCH 02/12] M8: wait for a recovery folder lock that frees itself (#334) A child process forked before its exec inherits the run's open descriptors, and a flock lasts while any descriptor for the file is open. A crashed run whose folder lock was still held by such a child read as a run that was still going, so recover_all skipped it and its unsaved documents were not opened at the next start. The desktop app's tests failed intermittently for the same reason whenever another test spawned a process in parallel (the reduced-motion read spawns gsettings on Linux and the BSDs): recovery_tests failed in 23 of 60 runs of the whole binary and never on its own. Taking a folder's lock now retries try_lock briefly, so a lock that frees itself is no longer read as a live run while a run that really is running still keeps its folder. Regression test: a_lock_freed_a_moment_later_is_not_a_run_that_is_still_going holds the crashed run's lock on a second descriptor, the way a forked child does, and frees it shortly after; the copies come back. The recovery tests that collide with a spawning test went from 8 failures in 10 runs to 0 in 10. --- apps/figcraft/src/recovery.rs | 29 +++++++++++++++++++++++------ apps/figcraft/src/recovery_tests.rs | 22 ++++++++++++++++++++++ docs/hardware-parity.md | 1 + 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/apps/figcraft/src/recovery.rs b/apps/figcraft/src/recovery.rs index 62243bc7..0ad97f0d 100644 --- a/apps/figcraft/src/recovery.rs +++ b/apps/figcraft/src/recovery.rs @@ -120,8 +120,7 @@ impl Recovery { (0..10).find_map(|n| { let dir = root.join(if n == 0 { pid.to_string() } else { format!("{pid}-{n}") }); std::fs::create_dir_all(&dir).ok()?; - let lock = File::options().write(true).create(true).truncate(false).open(dir.join(LOCK)).ok()?; - lock.try_lock().ok()?; + let lock = lock_briefly(&dir.join(LOCK))?; Some(Self { dir, _lock: lock, written: HashMap::new(), next: 0.0, waiting: Vec::new() }) }) } @@ -293,6 +292,27 @@ fn remove_kept(w: Waiting) { /// The extension of the note beside the copy of a document that is one of your online files. const ONLINE: &str = "online"; +/// How long a folder's lock that frees itself is waited for, and between the tries. +const LOCK_WAIT: (usize, std::time::Duration) = (20, std::time::Duration::from_millis(5)); + +/// Lock the folder's `file`, waiting briefly for a lock that frees itself. A child between its +/// `fork` and its `exec` inherits the run's open descriptors, and a `flock` lasts while any +/// descriptor for the file is open, so a run that has ended can look like one that is still going +/// for the length of that window. A run that really is running holds the lock for as long as it +/// runs, and we give up on it after [`LOCK_WAIT`] (#334). +fn lock_briefly(file: &Path) -> Option { + let lock = File::options().write(true).create(true).truncate(false).open(file).ok()?; + let (tries, pause) = LOCK_WAIT; + for _ in 0..tries { + match lock.try_lock() { + Ok(()) => return Some(lock), + Err(std::fs::TryLockError::WouldBlock) => std::thread::sleep(pause), + Err(_) => return None, + } + } + None +} + /// The copy at `file` is of one of your online files: join that file again as its owner, keeping /// the copy to compare with what arrives. The session, or `None` when the copy isn't one (or its /// file isn't recorded any more, or is open already): then it opens as any other copy. @@ -369,10 +389,7 @@ pub fn recover_all(root: &Path, s: &mut Session) -> Recovered { continue; } // Still running (another window of the app), or not ours to take. - let Ok(lock) = File::options().write(true).create(true).truncate(false).open(path.join(LOCK)) else { continue }; - if lock.try_lock().is_err() { - continue; - } + let Some(lock) = lock_briefly(&path.join(LOCK)) else { continue }; let lock = Arc::new(lock); let files: Vec = std::fs::read_dir(&path).into_iter().flatten().flatten().map(|f| f.path()).collect(); for f in &files { diff --git a/apps/figcraft/src/recovery_tests.rs b/apps/figcraft/src/recovery_tests.rs index 8f384b87..c05e86d0 100644 --- a/apps/figcraft/src/recovery_tests.rs +++ b/apps/figcraft/src/recovery_tests.rs @@ -203,6 +203,28 @@ fn a_run_whose_folder_name_is_still_held_takes_another() { let _ = std::fs::remove_dir_all(&root); } +/// #334: a lock that frees itself is not a run that is still going. A child between its `fork` and +/// its `exec` inherits the run's open descriptors and a `flock` lasts while any descriptor for the +/// file is open, so a crashed run's copies must still come back once that child has exec'd. +#[test] +fn a_lock_freed_a_moment_later_is_not_a_run_that_is_still_going() { + let root = root("fork-window"); + crashed(&edited(), &root); + // Hold the crashed run's folder lock on a second descriptor, the way a child forked before its + // exec holds it, and free it shortly after. + let lock = std::fs::File::options().write(true).create(true).truncate(false).open(root.join("crashed").join("lock")).unwrap(); + lock.try_lock().unwrap(); + let freed = std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(40)); + drop(lock); + }); + // The next start waits for that lock rather than reading the run as still going. + let mut next = Session::empty(); + assert_eq!(recover(&root, &mut next), 1, "the copies come back once the lock frees itself"); + freed.join().unwrap(); + let _ = std::fs::remove_dir_all(&root); +} + /// The rectangles in `doc`. fn rectangles(doc: &figcraft_engine::doc::Document) -> usize { doc.nodes().filter(|n| matches!(n.kind, figcraft_engine::doc::NodeKind::Rectangle)).count() diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md index df822981..4780a0a8 100644 --- a/docs/hardware-parity.md +++ b/docs/hardware-parity.md @@ -50,6 +50,7 @@ Nothing has been timed against Figma on the same document (needs `.fig` import). | Date | Change | Summary | |---|---|---| +| 2026-10-18 | minor | Recovery copies and the fork/exec window (#334): a child process forked before its `exec` (the `gsettings` read of the reduced-motion setting, any spawn) inherits the running app's open recovery-folder lock, and a `flock` lasts while any descriptor for the file is open. A crashed run whose lock was still held by such a child therefore read as "still running" and its unsaved documents were not recovered at the next start, and the desktop app's tests failed intermittently whenever another test spawned a process in parallel. Taking a folder's lock now waits briefly for one that frees itself, so only a run that is really running keeps its folder. No numbers changed | | 2026-10-18 | minor | macOS quit crash (#781): on Macs with a Touch Bar the app could crash as it quit (an exception in AppKit's Touch Bar support while the window's content view, which the accessibility adapter subclasses, was torn down). Quitting now keeps everything (preferences, this run's recovery copies, live sessions ended) and then ends the process before that teardown. Quit from the menu bar ends the app without the unsaved-changes question, so unsaved files' recovery copies now stay and reopen at the next start instead of being removed. Numbers unchanged | | 2026-10-17 | minor | Linux row: clipboard reads on Linux and the BSDs time out after 2 s on a helper thread, images copied in other apps paste under Wayland (arboard's data-control feature, #693), a minimised window can't be detected under Wayland, so frame-only control requests may still wait there (#720), and Wayland compositors without server-side decorations give the window Adwaita controls (#592, checked by its author on GNOME, not by the maintainers; KDE not run); compiled in CI, not run on a Linux desktop. HiDPI row: native-density canvas images placed on whole device pixels (#359, geometry tests; not judged by eye). Numbers unchanged | | 2026-10-14 | minor | Copy and paste in the browser: the web build never wrote to the system clipboard, so Cmd+V after Cmd+C pasted whatever text the clipboard held before (as a text layer) instead of the copied layers. Copies (layers, Copy as SVG/CSS/text/PNG, links) now go out through egui's output, which eframe writes during the copy event (`web_clipboard_tests`). Numbers unchanged. | From 13fdae3d44ea27780c2e273d4aa65d9b3379b59c Mon Sep 17 00:00:00 2001 From: Ashsajal Date: Sun, 18 Oct 2026 14:55:12 +0600 Subject: [PATCH 03/12] M9: the other item lists merge one by item when a branch merges (gap 17) Fills, strokes and effects already merged item by item, so both sides changing different items of one of those lists was no conflict. Every other list conflicted as a whole: both sides editing different export settings, or different guides, or different prototype interactions, or different accessibility annotations or Dev Mode links, each had to be resolved by hand for the whole list, though the items stand on their own. Export settings, guides, prototype interactions, accessibility annotations and the Dev Mode code links and resources now merge item by item, with the same rule: the same item changed both ways, an item removed on one side, or different items added on both sides still conflict. Properties that are keyed maps rather than item lists (bound, varModes, overrides, propRefs, pluginData) are deliberately left out: their items line up by key, not by position. Text runs are left whole too: they live in the node's text data, so merging them item by item means splitting a property the change format keeps whole. Replaces the test that documented the gap (other_lists_still_conflict_as_a_whole) with one that checks each new list merges, and adds one for the same item changed both ways still conflicting. --- crates/collab/src/merge.rs | 27 +++++++++--- crates/collab/src/merge_list_tests.rs | 61 +++++++++++++++++++++++---- docs/collaboration-parity.md | 5 ++- docs/gaps.md | 11 ++++- 4 files changed, 86 insertions(+), 18 deletions(-) diff --git a/crates/collab/src/merge.rs b/crates/collab/src/merge.rs index bb18e661..381b4393 100644 --- a/crates/collab/src/merge.rs +++ b/crates/collab/src/merge.rs @@ -4,10 +4,12 @@ //! //! A conflict is the same property (or document-level entry) changed on both sides to different //! values, or a layer deleted on one side while the other side edited it or something inside it -//! (including adding layers to it or moving layers into or out of it). Fills, strokes and effects -//! merge item by item: both sides changing different items of the list (or one adding items at its -//! end) is no conflict, only the same item changed both ways or the list shortened on a side -//! while the other changed it. Nothing that conflicts is +//! (including adding layers to it or moving layers into or out of it). The layer lists whose items +//! stand on their own (fills, strokes, effects, export settings, guides, prototype interactions, +//! accessibility annotations and the Dev Mode links and resources) merge item by item: both sides +//! changing different items of the list (or one adding items at its end) is no conflict, only the +//! same item changed both ways or the list shortened on a side while the other changed it. Nothing +//! that conflicts is //! merged until it is resolved: each conflict is resolved for one side, and the other side's //! changes to it are left out. Everything else from both sides is kept. @@ -159,8 +161,21 @@ fn by_key(changes: &[Change]) -> BTreeMap> { out } -/// List properties whose items merge one by one. -const PER_ITEM: &[Prop] = &[Prop::Fills, Prop::Strokes, Prop::Effects]; +/// List properties whose items merge one by item: the items stand on their own, so both sides +/// changing different ones (or one side adding at its end) is no conflict. Properties that are +/// keyed maps rather than item lists (`bound`, `varModes`, `overrides`, `propRefs`, `pluginData`) +/// are not here: their items line up by key, not by position. +const PER_ITEM: &[Prop] = &[ + Prop::Fills, + Prop::Strokes, + Prop::Effects, + Prop::Exports, + Prop::Guides, + Prop::Interactions, + Prop::Annotations, + Prop::CodeLinks, + Prop::DevResources, +]; /// Both sides' writes `b` and `m` of list property `key` merged item by item against the base /// (see [`merge_items`]); `None` when `key` isn't such a list or the writes conflict. diff --git a/crates/collab/src/merge_list_tests.rs b/crates/collab/src/merge_list_tests.rs index 436e6b6f..8b237145 100644 --- a/crates/collab/src/merge_list_tests.rs +++ b/crates/collab/src/merge_list_tests.rs @@ -1,5 +1,7 @@ -//! Fills, strokes and effects merge item by item: different items changed on each side merge, -//! the same item changed both ways conflicts. +//! The layer lists whose items stand on their own (fills, strokes, effects, export settings, +//! guides, prototype interactions, accessibility annotations, Dev Mode links and resources) merge +//! item by item: different items changed on each side merge, the same item changed both ways +//! conflicts. use figcraft_color::{Color, Paint}; use figcraft_doc::{Document, Effect, NodeId, NodeKind}; @@ -100,19 +102,60 @@ fn the_same_item_changed_both_ways_conflicts() { } #[test] -fn other_lists_still_conflict_as_a_whole() { - // Docs silent: lists other than fills, strokes and effects (export settings here) still - // conflict whole when both sides change them. +fn the_other_item_lists_merge_one_by_one_too() { + // Export settings, guides, prototype interactions, accessibility annotations and the Dev Mode + // links and resources are item lists like fills: both sides changing different items merges. let (mut base, _, _, r) = files(); let export = |suffix: &str| figcraft_doc::ExportSetting { suffix: suffix.into(), ..Default::default() }; - base.get_mut(r).unwrap().exports = vec![export("@1"), export("@2")]; + let guide = |vertical: bool, offset: f64| figcraft_doc::Guide { vertical, offset }; + let annot = |label: &str| figcraft_doc::Annotation { label: label.into(), properties: Vec::new(), category: None }; + let link = + |name: &str| figcraft_doc::CodeLink { language: "react".into(), import_path: String::new(), name: name.into(), template: String::new() }; + let res = |url: &str| figcraft_doc::DevResource { url: url.into(), name: String::new() }; + let n = base.get_mut(r).unwrap(); + n.exports = vec![export("@1"), export("@2")]; + n.guides = vec![guide(true, 10.0), guide(false, 20.0)]; + n.annotations = vec![annot("a1"), annot("a2")]; + n.code_links = vec![link("L1"), link("L2")]; + n.dev_resources = vec![res("https://a.test"), res("https://b.test")]; let (mut branch, mut main) = (base.clone(), base.clone()); branch.set_client_id(base.highest_id_space() + 1); - branch.get_mut(r).unwrap().exports[0] = export("@b"); - main.get_mut(r).unwrap().exports[1] = export("@m"); + // The branch changes the first item of each list; main the second. + let b = branch.get_mut(r).unwrap(); + (b.exports[0].suffix, b.guides[0].offset, b.annotations[0].label, b.code_links[0].name, b.dev_resources[0].url) = + ("@b".into(), 11.0, "b1".into(), "LB".into(), "https://branch.test".into()); + let m = main.get_mut(r).unwrap(); + (m.exports[1].suffix, m.guides[1].offset, m.annotations[1].label, m.code_links[1].name, m.dev_resources[1].url) = + ("@m".into(), 22.0, "m2".into(), "LM".into(), "https://main.test".into()); + both_ways(&base, &branch, &main, |doc| { + let n = doc.node(r).unwrap(); + let suffixes: Vec<&str> = n.exports.iter().map(|e| e.suffix.as_str()).collect(); + assert_eq!(suffixes, ["@b", "@m"], "export settings"); + let offsets: Vec = n.guides.iter().map(|g| g.offset).collect(); + assert_eq!(offsets, [11.0, 22.0], "guides"); + let labels: Vec<&str> = n.annotations.iter().map(|a| a.label.as_str()).collect(); + assert_eq!(labels, ["b1", "m2"], "annotations"); + let names: Vec<&str> = n.code_links.iter().map(|l| l.name.as_str()).collect(); + assert_eq!(names, ["LB", "LM"], "code links"); + let urls: Vec<&str> = n.dev_resources.iter().map(|d| d.url.as_str()).collect(); + assert_eq!(urls, ["https://branch.test", "https://main.test"], "dev resources"); + }); +} + +#[test] +fn the_same_item_of_another_list_changed_both_ways_conflicts() { + let (mut base, _, _, r) = files(); + base.get_mut(r).unwrap().exports = vec![ + figcraft_doc::ExportSetting { suffix: "@1".into(), ..Default::default() }, + figcraft_doc::ExportSetting { suffix: "@2".into(), ..Default::default() }, + ]; + let (mut branch, mut main) = (base.clone(), base.clone()); + branch.set_client_id(base.highest_id_space() + 1); + branch.get_mut(r).unwrap().exports[0].suffix = "@b".into(); + main.get_mut(r).unwrap().exports[0].suffix = "@m".into(); let p = plan(&base, &branch, &main).unwrap(); let keys: Vec = p.conflicts.iter().map(|c| c.key.to_string()).collect(); - assert_eq!(keys, [format!("{r}/exports")]); + assert_eq!(keys, [format!("{r}/exports")], "the same item changed both ways still conflicts"); } #[test] diff --git a/docs/collaboration-parity.md b/docs/collaboration-parity.md index ae503633..920a9621 100644 --- a/docs/collaboration-parity.md +++ b/docs/collaboration-parity.md @@ -18,7 +18,7 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla | Share links, permissions (view / edit), teams and projects | yes | **partial**: sharing with a join code from the Share popover (online: the file goes to the server in Preferences and the server names its edit and view links with short codes; local network: the app hosts and the code carries its address; one field joins with a code or a link, in the popover and on the home screen; the browser build also joins from a link in its URL's fragment, `#join=…`, which never reaches the web host's logs, capped at 6 KiB and checked like a `figcraft://` link (#712; unit-tested, not run in a browser)); `figcraft-server` share links per file with edit or view access (a viewer sees changes and presence, a "View only" badge and only the moving tools; the server refuses its edits), revocable at once, tokens stored as hashes; managed from the app's Share popover (first links), the server's command line or a managing connection. On a server open to the public, whoever published a file manages it with its owner token (links, name, removal: `collab.manageFile`), which the app remembers: Home ▸ Online lists your online files (open again as owner, start and stop sharing, copy the code, rename, remove, forget), with one request per server for what became of them; anyone in a file can report it, and the operator blocks addresses. The person hosting a session, or a file's owner, removes someone from it (right-click their avatar, `collab.removePerson`): they are disconnected and their address can't join that file again while it runs. The list is per computer: no accounts, teams or projects | 8–17 | | Comments: pins on frames and layers, threads, reactions, @mentions, resolve, unread, search and filters, links, in the presenter | yes (shared) | **done, local**: stored in the file, your name from Preferences; a behaviour suite from the documentation (pinning and following layers, threads, own edits and deletes, mentions, reactions, C and Shift+C, stepping, filters, saving, copy and paste); restoring a version keeps them. In a live session threads sync and anyone in it can reply, react and resolve (no accounts: a name is who you are) | (shared once multiplayer lands) | | Version history: named versions, autosave checkpoints, view before restore, restore, duplicate | yes (cloud) | **done**, saved in the `.figcraft` file: restoring keeps the replaced design as an autosave first, a titled autosave becomes a named version, merging a branch records the file before and after. Versions are per person in a live session (a shared history needs accounts); no comparing two versions (Dev Mode compares a ready-for-dev layer with its state when marked) | — | -| Branching and merging | yes (paid plans) | **done, local**: branches saved in the `.figcraft` file, each opened in its own tab; Review and merge shows both sides' changes since the branch was made and the conflicts (the same property on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merge as one undo step (the branch is archived, the main file's history gets a version for the merge), update from main, archive. Each branch keeps its own version history (saved, viewed and restored on the branch tab, kept when it is merged or archived); fills, strokes and effects merge item by item. Left: branches in live sessions; other lists (text runs, export settings) conflict as a whole | 1–2 | +| Branching and merging | yes (paid plans) | **done, local**: branches saved in the `.figcraft` file, each opened in its own tab; Review and merge shows both sides' changes since the branch was made and the conflicts (the same property on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merge as one undo step (the branch is archived, the main file's history gets a version for the merge), update from main, archive. Each branch keeps its own version history (saved, viewed and restored on the branch tab, kept when it is merged or archived); fills, strokes, effects, export settings, guides, prototype interactions, accessibility annotations and the Dev Mode links and resources merge item by item. Left: branches in live sessions; text runs conflict as a whole | 1–2 | | Team / organization libraries | yes | **partial**: a file's library is published to `figcraft-server` under the file and enabled in other files by a share link (per-asset versions, background update checks, overrides kept), usage counts per component and file, swapping a library by names ([components-parity](components-parity.md), [server.md](server.md#team-libraries)). No team-wide library list, accounts or analytics over time | 4–8 | | Notifications, activity | yes | no | with sharing | @@ -35,6 +35,9 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla | 2026-10-12 | minor | Public `figcraft-server` hardened against resource exhaustion after a security review: messages capped by what a connection is for (a file's size when open), byte-bounded send queues and a bound on bytes waiting for the hub, files checked as they grow (refused batches roll back with the reason; per-address and total caps count growth), the core's recent-batch memory bounded in bytes, no confirmation for changes that couldn't be written, team libraries only in key-published files on public servers with unpacking bounded by the file cap, usage reports kept in memory, layered IPv6 limits (/64, /56, /48), a global publish rate, connections reserved for sessions, per-address download shares, library connections cut on revoke and removal, a private data directory and key, and safer install.sh, systemd and backups. Percentages and hours unchanged | | 2026-10-12 | minor | A public `figcraft-server` (open hosting): with `open_publish = true` anyone publishes a file without the server key and gets its edit and view links plus an owner token (128 bits, shown once, stored hashed) that lists, makes and revokes its links, renames it or removes it (`collab.manageFile`); limits per address (connections at once and a minute, messages a second, publishes an hour, files and MiB kept; IPv6 by /64; `X-Forwarded-For` believed only from `trusted_proxy`; tables capped at 10,000 addresses) and for everyone (`max_total_mb`), each refusal saying why; files published without the key expire after `idle_days` unopened (the date shown to everyone in the file); moderation (`files list --sort size|age|idle`, `block-ip`, `reports`, a Report button and `collab.report`); the operator's contact and terms shown in the Share dialog; `GET /healthz` on the same port; `packaging/server/install.sh` for a Debian/Ubuntu machine behind Caddy and a nightly backup timer. Protocol additions within version 2, gated like signals (older apps get clear refusals). Hosting infrastructure rather than a Figma feature: percentages and hours unchanged | | 2026-10-12 | minor | Cursor chat and spotlight in live sessions: `/` opens a chat bubble at your pointer that everyone sees as you type and that fades a few seconds after the last change (Enter sends, Escape takes it back, 100 characters, control characters dropped); your avatar's menu (or `collab.spotlight`) asks everyone to follow your view and page, latecomers included, until they move their own view. Both travel as signals: ephemeral presence the host validates, rate-limits (20 a second per person) and passes only to clients that announce they read them, so older clients keep working; never in the document or the log. Live cursors / follow / spotlight row partial → done, 8–15 h → 2–4 h; cursor chat row no → partial (no audio or reactions), 4–8 h → 2–5 h; multiplayer 45–90 h → 42–84 h (it counted these rows only in part); remaining 55–110 h (stale: team libraries had already gone to 4–8 h) → 46–92 h; collaboration ~13% unchanged at this rounding | +| Date | Change | Summary | +|---|---|---| +| 2026-10-18 | minor | Branch merge, gap 17: the layer lists whose items stand on their own merge item by item, not just fills, strokes and effects. Export settings, guides, prototype interactions, accessibility annotations and the Dev Mode code links and resources now merge item by item, each conflicting only when the same item changed on both sides, an item was removed on one side, or both sides added different items. Text runs are left whole (they live in the node's text data). Numbers unchanged | | 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h | | 2026-10-12 | minor | Comments and version history behaviour suite written from the public documentation (15 tests, about 80 table rows plus stepwise checks; 4 tests failed before the fixes) and its fixes: in a live session, threads made in the session couldn't be replied to, edited, reacted to, resolved, moved, deleted or opened (their ids, from their creator's id space, were refused as out of range); restoring a version brought back that version's comments, losing newer threads and replies (comments now stay, those on layers the version lacks staying where they were); restoring now keeps the replaced design in the history as an autosave first, unless a version of it was just saved or restored; giving an autosave a title makes it a named version (it could be dropped to make room); merging a branch adds the main file as it was and then a `Merged “”` version to its history. Pinned where the documentation is silent: pins go to the innermost frame under them, copies, duplicates and duplicated pages come without comments, replying reopens a resolved thread, versions stay with whoever saved them in a live session (a shared history needs accounts). Left: a branch's own version history, comparing two versions; percentages and hours unchanged here (comments and versions are graded with feature area 16 in target-app-parity) | | 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | diff --git a/docs/gaps.md b/docs/gaps.md index 9b1a770c..dd42a75b 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -397,8 +397,14 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe different items changed on each side, or items added at the end on one side, merge without a conflict; only the same item changed both ways, an item removed on one side, or different items added on both sides conflict (as the whole list). -- **Missing:** branches of a file in a live session are refused; other lists (text runs, export - settings, guides…) still conflict as a whole; the branch history has no entries of its own for +- **Landed (2026-10-18):** the other layer lists whose items stand on their own merge item by item + too: export settings, guides, prototype interactions, accessibility annotations and the Dev Mode + code links and resources, each with the same rule as fills (the same item changed both ways still + conflicts). Properties that are keyed maps rather than item lists (`bound`, `varModes`, + `overrides`, `propRefs`, `pluginData`) deliberately do not: their items line up by key. +- **Missing:** branches of a file in a live session are refused; text runs still conflict as a + whole (they live in the node's text data, so merging them means splitting a property the change + format keeps whole); the branch history has no entries of its own for its creation or updates from main (only saved versions, autosaves and the merge on main). - **Estimate:** 1–2 h. @@ -444,6 +450,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe | Date | Change | Summary | |---|---|---| +| 2026-10-18 | minor | Gap 17: export settings, guides, prototype interactions, accessibility annotations and the Dev Mode code links and resources merge item by item like fills, strokes and effects (the same item changed both ways still conflicts). Text runs are left whole: they live in the node's text data, so merging them means splitting a property the change format keeps whole. Ranking and hours unchanged | | 2026-10-17 | minor | The 2026-10-17 landings. Gap 1: a new Landed entry (community batches #727 and #728, then #745: instance identity and cached library components, overrides by key, text style precedence, grid track sizes, slot content, labels of connectors saved with their path, skip ink, paragraph options, list start numbers, vertical trim, per-point stroke joins, text on a path, image adjustments, images in the message, instance layers' size and resizing overrides, Hug where nothing is recorded; budgets from the file's size; the import report; a `.fig` copy from Save as; paste from another design app), its Missing list cut to what is still left, and the wrong "whether other apps open its files is unknown" replaced by what was checked; all on synthetic files, so the beta blocker and hours stand. Gap 2: server limits, log recovery and web join links in the evidence. Gap 4: menus, the Actions palette and the editor fixes (#733, #735, #736, #729), then Selection colors and smart-selection spacing (#753) landed, none compared side by side. Gap 5: catalog counts re-measured (2,272 entries; drafts at 95–99%; Indonesian, Czech, Turkish and Swedish listed). Gap 8: the Linux and BSD clipboard changes and Wayland window controls, not run there by the maintainers. Ranking and hours unchanged | | 2026-10-15 | minor | Gap 7: bounded canvas overscan for smoother panning. Estimates and ranking unchanged. | | 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. | From d7a27c593ed15ad3ea8654bce15971fde7ac0c23 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 16:13:34 +0700 Subject: [PATCH 04/12] #795: wait once at startup for held recovery folders, only on Unix Collect the folders whose lock is held, wait 100 ms once and retry only those, instead of up to 100 ms per held folder. Recovery::start takes its folder without waiting again (a held name already falls through to another). Windows doesn't pass the lock to child processes, so the wait and its test are Unix-only; the test holds the lock 10 ms against the 100 ms wait. --- apps/figcraft/src/recovery.rs | 46 ++++++++++++++++------------- apps/figcraft/src/recovery_tests.rs | 3 +- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/apps/figcraft/src/recovery.rs b/apps/figcraft/src/recovery.rs index 0ad97f0d..06b0cbeb 100644 --- a/apps/figcraft/src/recovery.rs +++ b/apps/figcraft/src/recovery.rs @@ -120,7 +120,7 @@ impl Recovery { (0..10).find_map(|n| { let dir = root.join(if n == 0 { pid.to_string() } else { format!("{pid}-{n}") }); std::fs::create_dir_all(&dir).ok()?; - let lock = lock_briefly(&dir.join(LOCK))?; + let lock = lock_folder(&dir).ok()?; Some(Self { dir, _lock: lock, written: HashMap::new(), next: 0.0, waiting: Vec::new() }) }) } @@ -292,25 +292,15 @@ fn remove_kept(w: Waiting) { /// The extension of the note beside the copy of a document that is one of your online files. const ONLINE: &str = "online"; -/// How long a folder's lock that frees itself is waited for, and between the tries. -const LOCK_WAIT: (usize, std::time::Duration) = (20, std::time::Duration::from_millis(5)); +/// How long the startup waits, once, for folder locks that may free themselves (#334). +#[cfg(unix)] +const LOCK_WAIT: std::time::Duration = std::time::Duration::from_millis(100); -/// Lock the folder's `file`, waiting briefly for a lock that frees itself. A child between its -/// `fork` and its `exec` inherits the run's open descriptors, and a `flock` lasts while any -/// descriptor for the file is open, so a run that has ended can look like one that is still going -/// for the length of that window. A run that really is running holds the lock for as long as it -/// runs, and we give up on it after [`LOCK_WAIT`] (#334). -fn lock_briefly(file: &Path) -> Option { - let lock = File::options().write(true).create(true).truncate(false).open(file).ok()?; - let (tries, pause) = LOCK_WAIT; - for _ in 0..tries { - match lock.try_lock() { - Ok(()) => return Some(lock), - Err(std::fs::TryLockError::WouldBlock) => std::thread::sleep(pause), - Err(_) => return None, - } - } - None +/// Lock the folder `dir` (`WouldBlock` while another process holds it). +fn lock_folder(dir: &Path) -> Result { + let lock = File::options().write(true).create(true).truncate(false).open(dir.join(LOCK)).map_err(std::fs::TryLockError::Error)?; + lock.try_lock()?; + Ok(lock) } /// The copy at `file` is of one of your online files: join that file again as its owner, keeping @@ -382,6 +372,7 @@ pub fn recover_all(root: &Path, s: &mut Session) -> Recovered { } } }; + let (mut free, mut held) = (Vec::new(), Vec::new()); for e in entries.flatten() { let path = e.path(); if !path.is_dir() { @@ -389,7 +380,22 @@ pub fn recover_all(root: &Path, s: &mut Session) -> Recovered { continue; } // Still running (another window of the app), or not ours to take. - let Some(lock) = lock_briefly(&path.join(LOCK)) else { continue }; + match lock_folder(&path) { + Ok(lock) => free.push((path, lock)), + Err(std::fs::TryLockError::WouldBlock) => held.push(path), + Err(_) => {} + } + } + // A child between its `fork` and its `exec` shares the run's open descriptors, and a `flock` + // lasts while any of them is open, so an ended run's folder can look held for a moment. Wait + // once, however many folders are held, and try those again (#334). Windows doesn't pass the + // lock to child processes. + #[cfg(unix)] + if !held.is_empty() { + std::thread::sleep(LOCK_WAIT); + free.extend(held.into_iter().filter_map(|path| lock_folder(&path).ok().map(|lock| (path, lock)))); + } + for (path, lock) in free { let lock = Arc::new(lock); let files: Vec = std::fs::read_dir(&path).into_iter().flatten().flatten().map(|f| f.path()).collect(); for f in &files { diff --git a/apps/figcraft/src/recovery_tests.rs b/apps/figcraft/src/recovery_tests.rs index c05e86d0..0322e237 100644 --- a/apps/figcraft/src/recovery_tests.rs +++ b/apps/figcraft/src/recovery_tests.rs @@ -206,6 +206,7 @@ fn a_run_whose_folder_name_is_still_held_takes_another() { /// #334: a lock that frees itself is not a run that is still going. A child between its `fork` and /// its `exec` inherits the run's open descriptors and a `flock` lasts while any descriptor for the /// file is open, so a crashed run's copies must still come back once that child has exec'd. +#[cfg(unix)] #[test] fn a_lock_freed_a_moment_later_is_not_a_run_that_is_still_going() { let root = root("fork-window"); @@ -215,7 +216,7 @@ fn a_lock_freed_a_moment_later_is_not_a_run_that_is_still_going() { let lock = std::fs::File::options().write(true).create(true).truncate(false).open(root.join("crashed").join("lock")).unwrap(); lock.try_lock().unwrap(); let freed = std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_millis(40)); + std::thread::sleep(std::time::Duration::from_millis(10)); drop(lock); }); // The next start waits for that lock rather than reading the run as still going. From 8d3a3e617167ce1ad5f7e2374065f252ed494d79 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 16:13:41 +0700 Subject: [PATCH 05/12] #795: drop the hardware-parity revision row (test-only effect; the maintainer records it) --- docs/hardware-parity.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md index 4780a0a8..df822981 100644 --- a/docs/hardware-parity.md +++ b/docs/hardware-parity.md @@ -50,7 +50,6 @@ Nothing has been timed against Figma on the same document (needs `.fig` import). | Date | Change | Summary | |---|---|---| -| 2026-10-18 | minor | Recovery copies and the fork/exec window (#334): a child process forked before its `exec` (the `gsettings` read of the reduced-motion setting, any spawn) inherits the running app's open recovery-folder lock, and a `flock` lasts while any descriptor for the file is open. A crashed run whose lock was still held by such a child therefore read as "still running" and its unsaved documents were not recovered at the next start, and the desktop app's tests failed intermittently whenever another test spawned a process in parallel. Taking a folder's lock now waits briefly for one that frees itself, so only a run that is really running keeps its folder. No numbers changed | | 2026-10-18 | minor | macOS quit crash (#781): on Macs with a Touch Bar the app could crash as it quit (an exception in AppKit's Touch Bar support while the window's content view, which the accessibility adapter subclasses, was torn down). Quitting now keeps everything (preferences, this run's recovery copies, live sessions ended) and then ends the process before that teardown. Quit from the menu bar ends the app without the unsaved-changes question, so unsaved files' recovery copies now stay and reopen at the next start instead of being removed. Numbers unchanged | | 2026-10-17 | minor | Linux row: clipboard reads on Linux and the BSDs time out after 2 s on a helper thread, images copied in other apps paste under Wayland (arboard's data-control feature, #693), a minimised window can't be detected under Wayland, so frame-only control requests may still wait there (#720), and Wayland compositors without server-side decorations give the window Adwaita controls (#592, checked by its author on GNOME, not by the maintainers; KDE not run); compiled in CI, not run on a Linux desktop. HiDPI row: native-density canvas images placed on whole device pixels (#359, geometry tests; not judged by eye). Numbers unchanged | | 2026-10-14 | minor | Copy and paste in the browser: the web build never wrote to the system clipboard, so Cmd+V after Cmd+C pasted whatever text the clipboard held before (as a text layer) instead of the copied layers. Copies (layers, Copy as SVG/CSS/text/PNG, links) now go out through egui's output, which eframe writes during the copy event (`web_clipboard_tests`). Numbers unchanged. | From 6743c682031fa1636b8fbdb118b484e6fd1d1bdd Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 16:14:27 +0700 Subject: [PATCH 06/12] #794: drop the Finnish-only tests that pin wordings (the generic catalog tests cover fi) --- crates/ui-egui/src/i18n_tests.rs | 40 -------------------------------- 1 file changed, 40 deletions(-) diff --git a/crates/ui-egui/src/i18n_tests.rs b/crates/ui-egui/src/i18n_tests.rs index ea85ad32..d4d81d7b 100644 --- a/crates/ui-egui/src/i18n_tests.rs +++ b/crates/ui-egui/src/i18n_tests.rs @@ -8,46 +8,6 @@ use super::*; /// Languages that must translate every UI string and command label. const COMPLETE: &[&str] = &["es"]; -#[test] -fn finnish_locale_tags_and_integer_plurals() { - let fi = resolve("fi", None); - assert_eq!(LANGS[fi].code, "fi"); - assert_eq!(LANGS[fi].name, "Suomi"); - assert!(LANGS[fi].draft, "AI-assisted Finnish still needs human review"); - let mut session = figcraft_engine::Session::new(); - session.execute("pref.language", &serde_json::json!({"language":"fi"})).unwrap(); - let saved = serde_json::to_string(&session.prefs).unwrap(); - let restored: figcraft_engine::Prefs = serde_json::from_str(&saved).unwrap(); - assert_eq!(restored.language, "fi"); - for locale in ["fi-FI", "fi_FI.UTF-8", "FI-fi", "fi"] { - assert_eq!(resolve("system", Some(locale)), fi, "{locale}"); - } - for (count, expected) in [(0, "0 tasoa"), (1, "1 taso"), (2, "2 tasoa"), (21, "21 tasoa"), (101, "101 tasoa")] { - assert_eq!(fill(counted(fi, "{n} layers", count), &[("n", &count)]), expected); - } - assert_eq!(fill(counted(fi, "{n} minutes ago", 1), &[("n", &1)]), "1 minuutti sitten"); - assert_eq!(fill(counted(fi, "{n} minutes ago", 2), &[("n", &2)]), "2 minuuttia sitten"); - assert_eq!(fill(counted(fi, "{n} people joined", 2), &[("n", &2)]), "2 henkilöä liittyi"); -} - -#[test] -fn finnish_keeps_interpolated_names_and_context_specific_labels() { - let fi = resolve("fi", None); - let text = |key| match lookup(fi, key).unwrap() { - Text::Plain(s) => *s, - _ => panic!("expected a plain Finnish label: {key}"), - }; - assert_eq!(text("Cancel"), "Peruuta"); - assert_eq!(text("Fill"), "Täyttö"); - assert_eq!(text("Fill|sizing"), "Täytä"); - assert_eq!(text("Join|stroke"), "Liitos"); - assert_eq!(text("Open|comments"), "Avoin"); - assert_eq!(fill(text("Actions for {name}"), &[("name", &"File {1}.figcraft")]), "Kohteen File {1}.figcraft toiminnot"); - assert!(lookup(fi, "My layer {name}.svg").is_none()); - assert!(matches!(find(LANGS[fi].commands, "edit.undo"), Some(Text::Plain("Kumoa")))); - assert!(matches!(find(LANGS[fi].menu, "file.import"), Some(Text::Plain("Tuo tiedostosta…")))); -} - /// Keys looked up from data rather than written as `tr("…")` in the code: the main menu's /// submenu names (the engine's menu catalog) and labels of engine and document enums. fn dynamic_keys() -> BTreeSet { From ddd42b36bd09bc654f7a6daf70f534549b086baf Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 16:14:27 +0700 Subject: [PATCH 07/12] #794: list Finnish with the other catalogs in translating.md instead of a status paragraph --- docs/translating.md | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/docs/translating.md b/docs/translating.md index 3700bab4..d8addcbc 100644 --- a/docs/translating.md +++ b/docs/translating.md @@ -15,12 +15,12 @@ Simplified. `pref.language {language}` does the same from the CLI, MCP and the control channel. The catalogs: English (`en`, the source), and Spanish (`es`, with the Latin American overlay -`es-419`), French (`fr`), German (`de`), Dutch (`nl`), Brazilian Portuguese (`pt-BR`), Japanese (`ja`), -Korean (`ko`), Simplified Chinese (`zh-Hans`), Czech (`cs`) and Indonesian (`id`), machine-drafted -and waiting for native speakers' review. Russian (`ru`, Русский) was reviewed and approved by -VanTan; its catalog keeps Frame, Dev Mode and Smart animate as familiar tool names and uses « » -quotes. Turkish (`tr`) was machine-drafted and reviewed by a native speaker, and Swedish (`sv`) -translates every UI string, command label and menu wording. +`es-419`), French (`fr`), German (`de`), Dutch (`nl`), Brazilian Portuguese (`pt-BR`), Japanese +(`ja`), Korean (`ko`), Simplified Chinese (`zh-Hans`), Czech (`cs`), Indonesian (`id`) and Finnish +(`fi`), machine-drafted and waiting for native speakers' review. Russian (`ru`, Русский) was +reviewed and approved by VanTan; its catalog keeps Frame, Dev Mode and Smart animate as familiar +tool names and uses « » quotes. Turkish (`tr`) was machine-drafted and reviewed by a native speaker, +and Swedish (`sv`) translates every UI string, command label and menu wording. ## The catalogs @@ -68,18 +68,6 @@ language also needs its script in `cjk::language`. ## Adding a language -Finnish (`fi`, Suomi) is an AI-assisted draft awaiting native-speaker review. Its initial catalog -covers the current 1,729 English UI keys, 567 command labels and 31 menu-specific wordings, including -42 plural entries. Counts use singular only for 1 (`1 taso`, `0/2/21 tasoa`); this already follows -the default integer rule. Context keys distinguish paint `Fill` (Täyttö), sizing `Fill|sizing` -(Täytä), stroke `Join|stroke` (Liitos) and comment status `Open|comments` (Avoin). -The build discovers `fi.toml` automatically; `fi-FI` and `fi_FI.UTF-8` resolve to it. Product names, -code identifiers and shortcut notation retain their identity. Like the other unreviewed catalogs, -Finnish remains a draft outside `COMPLETE`; future English additions may fall back until translated. -Noise modes Mono/Duo/Multi describe one, two or many grain colours and read Yksivärinen, -Kaksivärinen and Monivärinen, independently of their stable enum names. -The narrow glass-effect light-intensity field uses Vahvuus so its complete label fits beside the value. - 1. Copy `crates/ui-egui/locales/es.toml` to `.toml` (a BCP 47 code: `fr`, `pt-BR`, `ja`) and set `[meta] name` and `status = "draft — needs native review"`. 2. Translate every value; keep the keys. Keep `{placeholders}` exactly, "…" where English has it, From d1125130f69c379a730a3d0bdb8b8cb70375c53f Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 16:14:45 +0700 Subject: [PATCH 08/12] #794: translate the slot-layers import note added on main --- crates/ui-egui/locales/fi.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui-egui/locales/fi.toml b/crates/ui-egui/locales/fi.toml index eb3ce94c..602574d7 100644 --- a/crates/ui-egui/locales/fi.toml +++ b/crates/ui-egui/locales/fi.toml @@ -1627,6 +1627,7 @@ status = "draft — needs native review" "set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" = "muuttujatilan asetustoiminnot ohitettiin, koska kokoelmia tai tiloja ei ole muuttujien JSON-tiedostossa" "slot content assignment target is missing or is not slot content" = "sisältöpaikan määrityksen kohde puuttuu tai ei ole sisältöpaikan sisältöä" "slot content assignment uses the empty GUID sentinel and was skipped" = "sisältöpaikan määritys käyttää tyhjää GUID-erikoisarvoa, ja se ohitettiin" +"slot layers without a matching slot property were imported as frames" = "sisältöpaikkatasot, joilla ei ole vastaavaa sisältöpaikkaominaisuutta, tuotiin kehyksinä" "some vector points refer to point settings missing from the file; they use the layer's joins and plain corners" = "jotkin vektoripisteet viittaavat tiedostosta puuttuviin pisteasetuksiin; ne käyttävät tason liitostyyliä ja tavallisia kulmia" "some vector segments refer to style settings missing from the file; they use the layer's stroke" = "jotkin vektorisegmentit viittaavat tiedostosta puuttuviin tyyliasetuksiin; ne käyttävät tason reunaviivaa" "stale or invalid grid track sizing entries were ignored" = "vanhentuneet tai virheelliset ruudukon kokomääritykset ohitettiin" From de2915908c94fe0ab707af9e6d1ce29d84f05cc8 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 17:01:02 +0700 Subject: [PATCH 09/12] #798: drop progress-doc edits (maintainer-owned) --- docs/collaboration-parity.md | 5 +---- docs/gaps.md | 11 ++--------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/docs/collaboration-parity.md b/docs/collaboration-parity.md index 920a9621..ae503633 100644 --- a/docs/collaboration-parity.md +++ b/docs/collaboration-parity.md @@ -18,7 +18,7 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla | Share links, permissions (view / edit), teams and projects | yes | **partial**: sharing with a join code from the Share popover (online: the file goes to the server in Preferences and the server names its edit and view links with short codes; local network: the app hosts and the code carries its address; one field joins with a code or a link, in the popover and on the home screen; the browser build also joins from a link in its URL's fragment, `#join=…`, which never reaches the web host's logs, capped at 6 KiB and checked like a `figcraft://` link (#712; unit-tested, not run in a browser)); `figcraft-server` share links per file with edit or view access (a viewer sees changes and presence, a "View only" badge and only the moving tools; the server refuses its edits), revocable at once, tokens stored as hashes; managed from the app's Share popover (first links), the server's command line or a managing connection. On a server open to the public, whoever published a file manages it with its owner token (links, name, removal: `collab.manageFile`), which the app remembers: Home ▸ Online lists your online files (open again as owner, start and stop sharing, copy the code, rename, remove, forget), with one request per server for what became of them; anyone in a file can report it, and the operator blocks addresses. The person hosting a session, or a file's owner, removes someone from it (right-click their avatar, `collab.removePerson`): they are disconnected and their address can't join that file again while it runs. The list is per computer: no accounts, teams or projects | 8–17 | | Comments: pins on frames and layers, threads, reactions, @mentions, resolve, unread, search and filters, links, in the presenter | yes (shared) | **done, local**: stored in the file, your name from Preferences; a behaviour suite from the documentation (pinning and following layers, threads, own edits and deletes, mentions, reactions, C and Shift+C, stepping, filters, saving, copy and paste); restoring a version keeps them. In a live session threads sync and anyone in it can reply, react and resolve (no accounts: a name is who you are) | (shared once multiplayer lands) | | Version history: named versions, autosave checkpoints, view before restore, restore, duplicate | yes (cloud) | **done**, saved in the `.figcraft` file: restoring keeps the replaced design as an autosave first, a titled autosave becomes a named version, merging a branch records the file before and after. Versions are per person in a live session (a shared history needs accounts); no comparing two versions (Dev Mode compares a ready-for-dev layer with its state when marked) | — | -| Branching and merging | yes (paid plans) | **done, local**: branches saved in the `.figcraft` file, each opened in its own tab; Review and merge shows both sides' changes since the branch was made and the conflicts (the same property on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merge as one undo step (the branch is archived, the main file's history gets a version for the merge), update from main, archive. Each branch keeps its own version history (saved, viewed and restored on the branch tab, kept when it is merged or archived); fills, strokes, effects, export settings, guides, prototype interactions, accessibility annotations and the Dev Mode links and resources merge item by item. Left: branches in live sessions; text runs conflict as a whole | 1–2 | +| Branching and merging | yes (paid plans) | **done, local**: branches saved in the `.figcraft` file, each opened in its own tab; Review and merge shows both sides' changes since the branch was made and the conflicts (the same property on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merge as one undo step (the branch is archived, the main file's history gets a version for the merge), update from main, archive. Each branch keeps its own version history (saved, viewed and restored on the branch tab, kept when it is merged or archived); fills, strokes and effects merge item by item. Left: branches in live sessions; other lists (text runs, export settings) conflict as a whole | 1–2 | | Team / organization libraries | yes | **partial**: a file's library is published to `figcraft-server` under the file and enabled in other files by a share link (per-asset versions, background update checks, overrides kept), usage counts per component and file, swapping a library by names ([components-parity](components-parity.md), [server.md](server.md#team-libraries)). No team-wide library list, accounts or analytics over time | 4–8 | | Notifications, activity | yes | no | with sharing | @@ -35,9 +35,6 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla | 2026-10-12 | minor | Public `figcraft-server` hardened against resource exhaustion after a security review: messages capped by what a connection is for (a file's size when open), byte-bounded send queues and a bound on bytes waiting for the hub, files checked as they grow (refused batches roll back with the reason; per-address and total caps count growth), the core's recent-batch memory bounded in bytes, no confirmation for changes that couldn't be written, team libraries only in key-published files on public servers with unpacking bounded by the file cap, usage reports kept in memory, layered IPv6 limits (/64, /56, /48), a global publish rate, connections reserved for sessions, per-address download shares, library connections cut on revoke and removal, a private data directory and key, and safer install.sh, systemd and backups. Percentages and hours unchanged | | 2026-10-12 | minor | A public `figcraft-server` (open hosting): with `open_publish = true` anyone publishes a file without the server key and gets its edit and view links plus an owner token (128 bits, shown once, stored hashed) that lists, makes and revokes its links, renames it or removes it (`collab.manageFile`); limits per address (connections at once and a minute, messages a second, publishes an hour, files and MiB kept; IPv6 by /64; `X-Forwarded-For` believed only from `trusted_proxy`; tables capped at 10,000 addresses) and for everyone (`max_total_mb`), each refusal saying why; files published without the key expire after `idle_days` unopened (the date shown to everyone in the file); moderation (`files list --sort size|age|idle`, `block-ip`, `reports`, a Report button and `collab.report`); the operator's contact and terms shown in the Share dialog; `GET /healthz` on the same port; `packaging/server/install.sh` for a Debian/Ubuntu machine behind Caddy and a nightly backup timer. Protocol additions within version 2, gated like signals (older apps get clear refusals). Hosting infrastructure rather than a Figma feature: percentages and hours unchanged | | 2026-10-12 | minor | Cursor chat and spotlight in live sessions: `/` opens a chat bubble at your pointer that everyone sees as you type and that fades a few seconds after the last change (Enter sends, Escape takes it back, 100 characters, control characters dropped); your avatar's menu (or `collab.spotlight`) asks everyone to follow your view and page, latecomers included, until they move their own view. Both travel as signals: ephemeral presence the host validates, rate-limits (20 a second per person) and passes only to clients that announce they read them, so older clients keep working; never in the document or the log. Live cursors / follow / spotlight row partial → done, 8–15 h → 2–4 h; cursor chat row no → partial (no audio or reactions), 4–8 h → 2–5 h; multiplayer 45–90 h → 42–84 h (it counted these rows only in part); remaining 55–110 h (stale: team libraries had already gone to 4–8 h) → 46–92 h; collaboration ~13% unchanged at this rounding | -| Date | Change | Summary | -|---|---|---| -| 2026-10-18 | minor | Branch merge, gap 17: the layer lists whose items stand on their own merge item by item, not just fills, strokes and effects. Export settings, guides, prototype interactions, accessibility annotations and the Dev Mode code links and resources now merge item by item, each conflicting only when the same item changed on both sides, an item was removed on one side, or both sides added different items. Text runs are left whole (they live in the node's text data). Numbers unchanged | | 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h | | 2026-10-12 | minor | Comments and version history behaviour suite written from the public documentation (15 tests, about 80 table rows plus stepwise checks; 4 tests failed before the fixes) and its fixes: in a live session, threads made in the session couldn't be replied to, edited, reacted to, resolved, moved, deleted or opened (their ids, from their creator's id space, were refused as out of range); restoring a version brought back that version's comments, losing newer threads and replies (comments now stay, those on layers the version lacks staying where they were); restoring now keeps the replaced design in the history as an autosave first, unless a version of it was just saved or restored; giving an autosave a title makes it a named version (it could be dropped to make room); merging a branch adds the main file as it was and then a `Merged “”` version to its history. Pinned where the documentation is silent: pins go to the innermost frame under them, copies, duplicates and duplicated pages come without comments, replying reopens a resolved thread, versions stay with whoever saved them in a live session (a shared history needs accounts). Left: a branch's own version history, comparing two versions; percentages and hours unchanged here (comments and versions are graded with feature area 16 in target-app-parity) | | 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | diff --git a/docs/gaps.md b/docs/gaps.md index dd42a75b..9b1a770c 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -397,14 +397,8 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe different items changed on each side, or items added at the end on one side, merge without a conflict; only the same item changed both ways, an item removed on one side, or different items added on both sides conflict (as the whole list). -- **Landed (2026-10-18):** the other layer lists whose items stand on their own merge item by item - too: export settings, guides, prototype interactions, accessibility annotations and the Dev Mode - code links and resources, each with the same rule as fills (the same item changed both ways still - conflicts). Properties that are keyed maps rather than item lists (`bound`, `varModes`, - `overrides`, `propRefs`, `pluginData`) deliberately do not: their items line up by key. -- **Missing:** branches of a file in a live session are refused; text runs still conflict as a - whole (they live in the node's text data, so merging them means splitting a property the change - format keeps whole); the branch history has no entries of its own for +- **Missing:** branches of a file in a live session are refused; other lists (text runs, export + settings, guides…) still conflict as a whole; the branch history has no entries of its own for its creation or updates from main (only saved versions, autosaves and the merge on main). - **Estimate:** 1–2 h. @@ -450,7 +444,6 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe | Date | Change | Summary | |---|---|---| -| 2026-10-18 | minor | Gap 17: export settings, guides, prototype interactions, accessibility annotations and the Dev Mode code links and resources merge item by item like fills, strokes and effects (the same item changed both ways still conflicts). Text runs are left whole: they live in the node's text data, so merging them means splitting a property the change format keeps whole. Ranking and hours unchanged | | 2026-10-17 | minor | The 2026-10-17 landings. Gap 1: a new Landed entry (community batches #727 and #728, then #745: instance identity and cached library components, overrides by key, text style precedence, grid track sizes, slot content, labels of connectors saved with their path, skip ink, paragraph options, list start numbers, vertical trim, per-point stroke joins, text on a path, image adjustments, images in the message, instance layers' size and resizing overrides, Hug where nothing is recorded; budgets from the file's size; the import report; a `.fig` copy from Save as; paste from another design app), its Missing list cut to what is still left, and the wrong "whether other apps open its files is unknown" replaced by what was checked; all on synthetic files, so the beta blocker and hours stand. Gap 2: server limits, log recovery and web join links in the evidence. Gap 4: menus, the Actions palette and the editor fixes (#733, #735, #736, #729), then Selection colors and smart-selection spacing (#753) landed, none compared side by side. Gap 5: catalog counts re-measured (2,272 entries; drafts at 95–99%; Indonesian, Czech, Turkish and Swedish listed). Gap 8: the Linux and BSD clipboard changes and Wayland window controls, not run there by the maintainers. Ranking and hours unchanged | | 2026-10-15 | minor | Gap 7: bounded canvas overscan for smoother panning. Estimates and ranking unchanged. | | 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. | From a4c7e259abf38485434249551478f7a459ab17b6 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 17:01:02 +0700 Subject: [PATCH 10/12] #798: layout grids merge item by item, code links merge whole (keyed by language); fix stale doc comments --- crates/collab/src/change.rs | 5 +++-- crates/collab/src/merge.rs | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/collab/src/change.rs b/crates/collab/src/change.rs index 32a49184..0608ba67 100644 --- a/crates/collab/src/change.rs +++ b/crates/collab/src/change.rs @@ -87,8 +87,9 @@ macro_rules! node_props { ($($variant:ident => $field:ident),* $(,)?) => { /// A node property: the tree position (`parent`, `position`) or one saved field of /// [`Node`]. List-valued fields (fills, text runs, vector networks…) are one property each, - /// replaced whole (three-way merges line fills, strokes and effects up item by item, see - /// [`merge`](crate::merge)). New properties go at the end (the binary form numbers them). + /// replaced whole (three-way merges line the lists whose items stand on their own, such as + /// fills, up item by item, see [`merge`](crate::merge)). New properties go at the end (the + /// binary form numbers them). #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum Prop { diff --git a/crates/collab/src/merge.rs b/crates/collab/src/merge.rs index 381b4393..5132ce75 100644 --- a/crates/collab/src/merge.rs +++ b/crates/collab/src/merge.rs @@ -5,13 +5,12 @@ //! A conflict is the same property (or document-level entry) changed on both sides to different //! values, or a layer deleted on one side while the other side edited it or something inside it //! (including adding layers to it or moving layers into or out of it). The layer lists whose items -//! stand on their own (fills, strokes, effects, export settings, guides, prototype interactions, -//! accessibility annotations and the Dev Mode links and resources) merge item by item: both sides +//! stand on their own (fills, strokes, effects, layout grids, export settings, guides, prototype +//! interactions, accessibility annotations and Dev Mode resources) merge item by item: both sides //! changing different items of the list (or one adding items at its end) is no conflict, only the //! same item changed both ways or the list shortened on a side while the other changed it. Nothing -//! that conflicts is -//! merged until it is resolved: each conflict is resolved for one side, and the other side's -//! changes to it are left out. Everything else from both sides is kept. +//! that conflicts is merged until it is resolved: each conflict is resolved for one side, and the +//! other side's changes to it are left out. Everything else from both sides is kept. use std::collections::{BTreeMap, BTreeSet}; use std::fmt; @@ -161,19 +160,21 @@ fn by_key(changes: &[Change]) -> BTreeMap> { out } -/// List properties whose items merge one by item: the items stand on their own, so both sides -/// changing different ones (or one side adding at its end) is no conflict. Properties that are -/// keyed maps rather than item lists (`bound`, `varModes`, `overrides`, `propRefs`, `pluginData`) -/// are not here: their items line up by key, not by position. +/// List properties whose items merge one by one: the items stand on their own, so both sides +/// changing different ones (or one side adding at its end) is no conflict. Sets and keyed maps +/// (`bound`, `varModes`, `overrides`, `propRefs`, `pluginData`) are not here: their items line up +/// by key, not by position. const PER_ITEM: &[Prop] = &[ Prop::Fills, Prop::Strokes, Prop::Effects, + Prop::Grids, Prop::Exports, Prop::Guides, Prop::Interactions, Prop::Annotations, - Prop::CodeLinks, + // Not `CodeLinks`: there is one per language, and lining them up by position could leave two + // for one language (the second hidden), so they merge whole. Prop::DevResources, ]; From 53c8308b66072aad100eba6626da7b33d29a406e Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 17:01:03 +0700 Subject: [PATCH 11/12] #798: test layout grids, code links and interactions (add, change, delete, conflicts) --- crates/collab/src/merge_list_tests.rs | 89 +++++++++++++++++++++------ 1 file changed, 71 insertions(+), 18 deletions(-) diff --git a/crates/collab/src/merge_list_tests.rs b/crates/collab/src/merge_list_tests.rs index 8b237145..e92f6bbb 100644 --- a/crates/collab/src/merge_list_tests.rs +++ b/crates/collab/src/merge_list_tests.rs @@ -1,10 +1,10 @@ -//! The layer lists whose items stand on their own (fills, strokes, effects, export settings, -//! guides, prototype interactions, accessibility annotations, Dev Mode links and resources) merge -//! item by item: different items changed on each side merge, the same item changed both ways +//! The layer lists whose items stand on their own (fills, strokes, effects, layout grids, export +//! settings, guides, prototype interactions, accessibility annotations and Dev Mode resources) +//! merge item by item: different items changed on each side merge, the same item changed both ways //! conflicts. use figcraft_color::{Color, Paint}; -use figcraft_doc::{Document, Effect, NodeId, NodeKind}; +use figcraft_doc::{Document, Effect, LayoutGrid, NodeId, NodeKind, proto}; use serde_json::{Value, json}; use crate::merge::{Key, Side, merge_items, plan}; @@ -103,30 +103,29 @@ fn the_same_item_changed_both_ways_conflicts() { #[test] fn the_other_item_lists_merge_one_by_one_too() { - // Export settings, guides, prototype interactions, accessibility annotations and the Dev Mode - // links and resources are item lists like fills: both sides changing different items merges. + // Layout grids, export settings, guides, accessibility annotations and Dev Mode resources are + // item lists like fills: both sides changing different items merges. let (mut base, _, _, r) = files(); let export = |suffix: &str| figcraft_doc::ExportSetting { suffix: suffix.into(), ..Default::default() }; let guide = |vertical: bool, offset: f64| figcraft_doc::Guide { vertical, offset }; let annot = |label: &str| figcraft_doc::Annotation { label: label.into(), properties: Vec::new(), category: None }; - let link = - |name: &str| figcraft_doc::CodeLink { language: "react".into(), import_path: String::new(), name: name.into(), template: String::new() }; + let grid = |size: f64| LayoutGrid { size, ..Default::default() }; let res = |url: &str| figcraft_doc::DevResource { url: url.into(), name: String::new() }; let n = base.get_mut(r).unwrap(); n.exports = vec![export("@1"), export("@2")]; n.guides = vec![guide(true, 10.0), guide(false, 20.0)]; n.annotations = vec![annot("a1"), annot("a2")]; - n.code_links = vec![link("L1"), link("L2")]; + n.grids = vec![grid(10.0), grid(20.0)]; n.dev_resources = vec![res("https://a.test"), res("https://b.test")]; let (mut branch, mut main) = (base.clone(), base.clone()); branch.set_client_id(base.highest_id_space() + 1); // The branch changes the first item of each list; main the second. let b = branch.get_mut(r).unwrap(); - (b.exports[0].suffix, b.guides[0].offset, b.annotations[0].label, b.code_links[0].name, b.dev_resources[0].url) = - ("@b".into(), 11.0, "b1".into(), "LB".into(), "https://branch.test".into()); + (b.exports[0].suffix, b.guides[0].offset, b.annotations[0].label, b.grids[0].size, b.dev_resources[0].url) = + ("@b".into(), 11.0, "b1".into(), 12.0, "https://branch.test".into()); let m = main.get_mut(r).unwrap(); - (m.exports[1].suffix, m.guides[1].offset, m.annotations[1].label, m.code_links[1].name, m.dev_resources[1].url) = - ("@m".into(), 22.0, "m2".into(), "LM".into(), "https://main.test".into()); + (m.exports[1].suffix, m.guides[1].offset, m.annotations[1].label, m.grids[1].size, m.dev_resources[1].url) = + ("@m".into(), 22.0, "m2".into(), 24.0, "https://main.test".into()); both_ways(&base, &branch, &main, |doc| { let n = doc.node(r).unwrap(); let suffixes: Vec<&str> = n.exports.iter().map(|e| e.suffix.as_str()).collect(); @@ -135,8 +134,8 @@ fn the_other_item_lists_merge_one_by_one_too() { assert_eq!(offsets, [11.0, 22.0], "guides"); let labels: Vec<&str> = n.annotations.iter().map(|a| a.label.as_str()).collect(); assert_eq!(labels, ["b1", "m2"], "annotations"); - let names: Vec<&str> = n.code_links.iter().map(|l| l.name.as_str()).collect(); - assert_eq!(names, ["LB", "LM"], "code links"); + let sizes: Vec = n.grids.iter().map(|g| g.size).collect(); + assert_eq!(sizes, [12.0, 24.0], "layout grids"); let urls: Vec<&str> = n.dev_resources.iter().map(|d| d.url.as_str()).collect(); assert_eq!(urls, ["https://branch.test", "https://main.test"], "dev resources"); }); @@ -145,17 +144,71 @@ fn the_other_item_lists_merge_one_by_one_too() { #[test] fn the_same_item_of_another_list_changed_both_ways_conflicts() { let (mut base, _, _, r) = files(); - base.get_mut(r).unwrap().exports = vec![ + let link = |language: &str| figcraft_doc::CodeLink { language: language.into(), ..Default::default() }; + let n = base.get_mut(r).unwrap(); + n.exports = vec![ figcraft_doc::ExportSetting { suffix: "@1".into(), ..Default::default() }, figcraft_doc::ExportSetting { suffix: "@2".into(), ..Default::default() }, ]; + n.code_links = vec![link("react"), link("vue")]; let (mut branch, mut main) = (base.clone(), base.clone()); branch.set_client_id(base.highest_id_space() + 1); branch.get_mut(r).unwrap().exports[0].suffix = "@b".into(); main.get_mut(r).unwrap().exports[0].suffix = "@m".into(); + // Code links are one per language, not an item list: different links changed still conflict. + branch.get_mut(r).unwrap().code_links[0].name = "B".into(); + main.get_mut(r).unwrap().code_links[1].name = "M".into(); let p = plan(&base, &branch, &main).unwrap(); - let keys: Vec = p.conflicts.iter().map(|c| c.key.to_string()).collect(); - assert_eq!(keys, [format!("{r}/exports")], "the same item changed both ways still conflicts"); + let mut keys: Vec = p.conflicts.iter().map(|c| c.key.to_string()).collect(); + keys.sort(); + let mut want = [format!("{r}/codeLinks"), format!("{r}/exports")]; + want.sort(); + assert_eq!(keys, want); +} + +#[test] +fn interactions_merge_item_by_item() { + let ia = |duration_ms: u32| proto::Interaction { + trigger: Default::default(), + actions: vec![proto::Action::Back], + transition: Default::default(), + duration_ms, + easing: Default::default(), + delay_ms: 0, + }; + let durations = |doc: &Document, r: NodeId| doc.node(r).unwrap().interactions.iter().map(|i| i.duration_ms).collect::>(); + let (mut base, _, _, r) = files(); + base.get_mut(r).unwrap().interactions = vec![ia(100), ia(200)]; + let fork = || { + let (mut branch, main) = (base.clone(), base.clone()); + branch.set_client_id(base.highest_id_space() + 1); + (branch, main) + }; + // The branch changes the first interaction and adds one; main changes the second. + let (mut branch, mut main) = fork(); + branch.get_mut(r).unwrap().interactions[0].duration_ms = 150; + branch.get_mut(r).unwrap().interactions.push(ia(300)); + main.get_mut(r).unwrap().interactions[1].duration_ms = 250; + both_ways(&base, &branch, &main, |doc| assert_eq!(durations(doc, r), [150, 250, 300])); + // The branch deletes one while main leaves them alone. + let (mut branch, mut main) = fork(); + branch.get_mut(r).unwrap().interactions.remove(0); + main.get_mut(r).unwrap().fills[0] = red(); + both_ways(&base, &branch, &main, |doc| assert_eq!(durations(doc, r), [200])); + // The same interaction changed both ways, or one deleted while the other side changed the + // list, conflicts. + for delete in [false, true] { + let (mut branch, mut main) = fork(); + if delete { + branch.get_mut(r).unwrap().interactions.remove(0); + } else { + branch.get_mut(r).unwrap().interactions[1].duration_ms = 220; + } + main.get_mut(r).unwrap().interactions[1].duration_ms = 250; + let p = plan(&base, &branch, &main).unwrap(); + let keys: Vec = p.conflicts.iter().map(|c| c.key.to_string()).collect(); + assert_eq!(keys, [format!("{r}/interactions")], "delete: {delete}"); + } } #[test] From adb23f0ff19470f43bb7872be2409a117b6ef477 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 17:01:03 +0700 Subject: [PATCH 12/12] #798: branch.review docs name the lists that merge item by item --- docs/mcp.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/mcp.md b/docs/mcp.md index 786fde0a..4f149503 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -972,10 +972,11 @@ branches). Driven with `run_command`: name, property, branch, main, deletedBy?}]}`. `layers` are the changed layers with their property changes formatted like Inspect (`fill #2563EB → #DC2626`); `document` lists styles, variables, comments and file settings. A conflict is the same property changed on both sides to different - values (key `/`, e.g. `4/fills`; fills, strokes and effects compare item by - item, so changing different items of one list on each side, or adding items at its end on one - side, merges without a conflict; a layer's parent and position are one - property, `parent`), a layer deleted on one side while the other edited it or what's inside it + values (key `/`, e.g. `4/fills`; fills, strokes, effects, layout grids, + export settings, guides, prototype interactions, annotations and Dev Mode resources compare item + by item, so changing different items of one list on each side, or adding items at its end on one + side, merges without a conflict; other lists, such as code links, compare whole; a layer's parent + and position are one property, `parent`), a layer deleted on one side while the other edited it or what's inside it (`/layer`, with `deletedBy`), or a document-level entry or setting changed on both sides (`style/`, `variable/`, `comment/`, `doc/`…). - `branch.merge {id?, resolutions?}` merges the branch into the main file as one undo step,