mirror of
https://github.com/penpot/penpot.git
synced 2026-09-23 20:36:15 +00:00
🌐 Complete Catalan translations in frontend (#11741)
* 🌐 Complete Catalan translations in frontend Complete the Catalan (ca.po) locale to 100% coverage against en.po, using es.po as support reference. Adds the 1439 missing entries across workspace, dashboard, labels, shortcuts, subscription, errors, modals and onboarding, keeping vosaltres treatment and IEC/Termcat terminology consistent with the existing strings. Normalizes placeholders and plural forms, drops the 14 stale obsolete entries and canonicalizes the file with the repo translations script. Closes #11739 AI-assisted-by: muse-spark-1.3-contributor * 📚 Add frontend translations memory with Catalan criteria Record the PO workflow, the sync fuzzy-flag gotcha and the Catalan glossary and tone agreed upon while completing ca.po, and link the new memory from the frontend core routing. AI-assisted-by: muse-spark-1.3-contributor * 🔧 Add gettext to devenv image Provide msgfmt and msgattrib in the dev environment for checking PO translation files. AI-assisted-by: muse-spark-1.3-contributor * 🌐 Fix Catalan translations and add PO checker Review of the missing-whitespace pattern found ~90 glued words across 75 entries, plus 4 lost plural forms and 2 placeholder mismatches verified against tr call sites. All fixed in ca.po. Adds frontend/scripts/check-translations.js (vocabulary-free PO QA: glued words, punctuation, placeholders, plurals) with --self-test, wired as pnpm run check-translations and documented in mem:frontend/translations. AI-assisted-by: muse-spark-1.3-contributor * 🌐 Multi-locale PO checker with word catalogs Split the checker engine from its word lists: ca/es catalogs now live in scripts/check-translations/words.<locale>.txt and all messages are in English. Adds an es seed (calibrated to zero errors) and fixes 7 typos it found in es.po. Universal checks (placeholders, plurals, punctuation) run without a catalog. AI-assisted-by: muse-spark-1.3-contributor * 🌐 Merge PO checker into translations.js Fold check-translations.js into translations.js as a check subcommand reusing its locale helpers; word lists stay in scripts/check-translations/words.<locale>.txt. Also fixes the getopts stopEarly bug that made -l useless after the command (sync -l ca synced every locale), drops dead lodash import and code, unifies help and exit codes. Removes the check-translations package alias; use translations.js check -l <locale> with explicit -l. AI-assisted-by: muse-spark-1.3-contributor * 🌐 Keep unused placeholders out of the gate Reverts the %s-stripping on unused auth.terms-privacy-agreement: the links mirror its markdown sibling and a reactivation may need them. Placeholder mismatches on #, unused keys now warn instead of failing, and the rule is recorded in mem:frontend/translations. AI-assisted-by: muse-spark-1.3-contributor
This commit is contained in:
parent
2255266d45
commit
bc3cb4bddf
@ -37,6 +37,7 @@ See `mem:scripts/paren-repair`.
|
||||
UI and packages:
|
||||
- App UI components, SCSS modules, style-system boundaries, accessibility, i18n, and render performance: `mem:frontend/ui-conventions-and-style-system`.
|
||||
- JS/TS packages, shared UI package, text editor, Storybook, and package builds: `mem:frontend/ui-packages-text-editor-workflow`.
|
||||
- PO translation workflow and per-locale conventions: `mem:frontend/translations`.
|
||||
|
||||
Workspace behavior:
|
||||
- Workspace state, commits, persistence, undo, repo calls, and refs: `mem:frontend/workspace-state-persistence-subtleties`.
|
||||
|
||||
82
.serena/memories/frontend/translations.md
Normal file
82
.serena/memories/frontend/translations.md
Normal file
@ -0,0 +1,82 @@
|
||||
# Frontend Translations
|
||||
|
||||
PO-based UI i18n. Files: `frontend/translations/*.po`. `en.po` is the
|
||||
source of truth (`msgid` = key, `msgstr` = English); `es.po` is a
|
||||
high-coverage support reference, never the base.
|
||||
|
||||
## Workflow
|
||||
|
||||
- Canonicalize with `node ./scripts/translations.js sync -l <locale>`
|
||||
from `frontend/`: sorts entries, syncs `#:` comments from `en`,
|
||||
deletes keys missing in `en`.
|
||||
- `sync` copies ALL comment flags from `en`, including `#, fuzzy`.
|
||||
Translated entries must NOT stay fuzzy: strip the flag after
|
||||
translating (mirror `es.po`, which keeps fuzzy only on genuinely
|
||||
untranslated entries). Fuzzy entries are excluded from
|
||||
`msgfmt --statistics` translated counts.
|
||||
- Canonical files carry NO `#~` obsolete blocks (`en`/`es` have zero).
|
||||
Drop them; `sync` does not resurrect them.
|
||||
- Definition of done: `msgfmt --check <locale>.po` exit 0 and
|
||||
`msgfmt --statistics` shows 0 fuzzy, 0 untranslated.
|
||||
|
||||
## Entry rules
|
||||
|
||||
- New entries take `#:` refs from `en`; copy `#, unused`, never `#, fuzzy`.
|
||||
- `en` keys with empty `msgstr` (or `#, fuzzy` + empty): translate from
|
||||
`es`/source context, never leave empty.
|
||||
- Entries whose `en` uses `msgid_plural` need `msgstr[0]`/`msgstr[1]`
|
||||
(header: `nplurals=2; plural=n != 1`). Keep the `msgid_plural` line:
|
||||
a singular `msgstr` on a plural key silently breaks count selection
|
||||
at runtime (the app build reads 1-elem `msgstr` as singular).
|
||||
- `sync` re-adds `#, fuzzy` on EVERY run for entries fuzzy in `en`;
|
||||
re-strip after the last sync, never before it.
|
||||
- Preserve verbatim: `%s`/`%d`, `{var}`/`{{...}}`, markdown
|
||||
`[text](%s)`, HTML tags, `\n` positions, brand names (Penpot),
|
||||
key names (Ctrl/Shift/Alt), technical terms (SVG, CSS, HSV, RGB).
|
||||
|
||||
## Catalan (ca) conventions
|
||||
|
||||
- Normative IEC/Termcat Catalan. Address the user in VOSALTRES
|
||||
(2nd person plural): "Deseu", "Creeu", "Ja teniu un compte?".
|
||||
Buttons/menus use short imperatives ("Crea", "Mou", "Restaura").
|
||||
- Established glossary (reuse exactly, do not re-coin): layer=capa,
|
||||
board=tauler, stroke=traç, fill=Emplenat, blur=Difuminat,
|
||||
shadow=Ombra, clipboard=porta-retalls, delete=Elimina,
|
||||
rename=Canvia el nom, shortcut=drecera, grid=graella (keep "grid"
|
||||
where the file already does, e.g. grid-layout editing),
|
||||
plugins/extensions UI=extensions, layout=Disposició,
|
||||
gradient=Degradat, wireframing kept as loanword.
|
||||
- Ela geminada uses the middle dot: Cancel·la, paral·lel, al·lega.
|
||||
- ALL-CAPS source stays ALL-CAPS in Catalan; keep `$175`-style
|
||||
amounts in IEC format (`175 $/mes`) only where `es` already adapts.
|
||||
- Shortcut/action names (`shortcuts.*`) are noun/infinitive labels,
|
||||
not sentences. Error strings are direct, no hedging.
|
||||
- Same English source in different contexts may legitimately differ
|
||||
(verb "Copia" vs noun "Còpia"; "Desactivat" vs "Deshabilitada"
|
||||
agreeing with "drecera"). Normalize only true duplicates.
|
||||
|
||||
## QA before commit
|
||||
|
||||
- Run `node ./scripts/translations.js check -l <locale>` from
|
||||
`frontend/` (no default locale: pass `-l` explicitly): 0 errors
|
||||
required; review warnings by hand. Word lists live in
|
||||
`frontend/scripts/check-translations/words.<locale>.txt`
|
||||
(`[elision]` `[function]` `[common]` `[ok]` `[brands]`); new valid
|
||||
words that trip the gate go to `[ok]`; `check --self-test`
|
||||
covers the detector rules. Without a catalog only the universal
|
||||
checks run. `#, fuzzy` entries are skipped (known-pending,
|
||||
owned elsewhere).
|
||||
- Placeholder parity per entry (singular AND each plural form,
|
||||
also enforced by the script); verify `%s` against the `tr` call
|
||||
site when `en`/`es`/code disagree (a `%s` the code never passes
|
||||
renders literally; a dropped one swallows the argument). On
|
||||
`#, unused` keys the script only warns: never "fix" them by
|
||||
deleting placeholders or links, a reactivation may need them.
|
||||
- Glued words (AI batches drop spaces at wrap boundaries): the
|
||||
script flags function-word splits (`del'equip`, `lapolítica`,
|
||||
`sinecessiteu`), `,/.`/`:` without following space,
|
||||
lowercase+Uppercase joins (`delPenpot`, `oCapitalize`) and `%s`
|
||||
glued to a word.
|
||||
- Balanced `[]`/`()` in markdown links; no double spaces; no glued
|
||||
words around `·`; trailing spaces match the source.
|
||||
- `git diff --stat` must touch only `frontend/translations/<locale>.po`.
|
||||
@ -476,6 +476,7 @@ RUN set -ex; \
|
||||
nginx \
|
||||
fd-find \
|
||||
bat \
|
||||
gettext \
|
||||
\
|
||||
fontconfig \
|
||||
woff-tools \
|
||||
|
||||
505
frontend/scripts/check-translations/words.ca.txt
Normal file
505
frontend/scripts/check-translations/words.ca.txt
Normal file
@ -0,0 +1,505 @@
|
||||
# Word catalog for check-translations.js (Catalan).
|
||||
# Comment lines start with #. Blank lines are ignored.
|
||||
# [function] short function words; a token starting with one of
|
||||
# these is likely missing a space (del-equip style).
|
||||
# [common] ordinary words used to judge the other half of a split.
|
||||
# [ok] correct words the splitter would flag; never report
|
||||
# these. Add new valid words here when the gate trips.
|
||||
# [brands] extra locale-specific brands/terms kept as-is.
|
||||
# [elision] letters elided with an apostrophe.
|
||||
|
||||
[elision]
|
||||
ldsmntc
|
||||
|
||||
[function]
|
||||
i
|
||||
o
|
||||
a
|
||||
e
|
||||
de
|
||||
del
|
||||
dels
|
||||
la
|
||||
el
|
||||
els
|
||||
les
|
||||
lo
|
||||
un
|
||||
una
|
||||
uns
|
||||
unes
|
||||
al
|
||||
als
|
||||
en
|
||||
amb
|
||||
per
|
||||
pel
|
||||
pels
|
||||
que
|
||||
com
|
||||
no
|
||||
ni
|
||||
si
|
||||
ja
|
||||
se
|
||||
es
|
||||
ho
|
||||
hi
|
||||
li
|
||||
me
|
||||
te
|
||||
ne
|
||||
em
|
||||
et
|
||||
us
|
||||
vos
|
||||
ens
|
||||
son
|
||||
són
|
||||
és
|
||||
més
|
||||
mes
|
||||
tot
|
||||
tota
|
||||
molt
|
||||
tan
|
||||
tant
|
||||
on
|
||||
quan
|
||||
perquè
|
||||
pero
|
||||
però
|
||||
doncs
|
||||
fins
|
||||
entre
|
||||
sobre
|
||||
sota
|
||||
cap
|
||||
cada
|
||||
altre
|
||||
seva
|
||||
seu
|
||||
meva
|
||||
teu
|
||||
nostre
|
||||
vostre
|
||||
aquest
|
||||
aquesta
|
||||
això
|
||||
allò
|
||||
jo
|
||||
tu
|
||||
ell
|
||||
ella
|
||||
nosaltres
|
||||
vosaltres
|
||||
ells
|
||||
|
||||
[common]
|
||||
corregir
|
||||
poder
|
||||
obtenir
|
||||
mantindran
|
||||
targeta
|
||||
controladors
|
||||
importants
|
||||
futura
|
||||
donant
|
||||
tingui
|
||||
informar
|
||||
diversitat
|
||||
continuï
|
||||
juntament
|
||||
revocaran
|
||||
habilitar
|
||||
desament
|
||||
autenticació
|
||||
family
|
||||
aplicació
|
||||
còpies
|
||||
configurat
|
||||
necessita
|
||||
conflictes
|
||||
trigar
|
||||
res
|
||||
directament
|
||||
enviï
|
||||
correccions
|
||||
admet
|
||||
exportacions
|
||||
aplicacions
|
||||
càrrec
|
||||
envia
|
||||
enllaç
|
||||
actuals
|
||||
ajuda
|
||||
moment
|
||||
valor
|
||||
equips
|
||||
conjunt
|
||||
projecte
|
||||
fitxer
|
||||
compte
|
||||
usuari
|
||||
persona
|
||||
persones
|
||||
cosa
|
||||
temps
|
||||
part
|
||||
text
|
||||
nom
|
||||
contrasenya
|
||||
sessió
|
||||
idioma
|
||||
versió
|
||||
canvi
|
||||
arxiu
|
||||
error
|
||||
avís
|
||||
filtre
|
||||
cerca
|
||||
vista
|
||||
pestanya
|
||||
botó
|
||||
camp
|
||||
llista
|
||||
taula
|
||||
imatge
|
||||
forma
|
||||
capa
|
||||
fons
|
||||
color
|
||||
mida
|
||||
data
|
||||
correu
|
||||
propietari
|
||||
propietària
|
||||
membre
|
||||
opcions
|
||||
prova
|
||||
suprimireu
|
||||
interactuïn
|
||||
ajudarem
|
||||
quedi
|
||||
configurat
|
||||
permetre
|
||||
continueu
|
||||
breu
|
||||
manteniment
|
||||
present
|
||||
començar
|
||||
donant
|
||||
css
|
||||
font
|
||||
prioritzar
|
||||
suborganitzacions
|
||||
podreu
|
||||
poden
|
||||
facturació
|
||||
|
||||
[ok]
|
||||
ajudarem
|
||||
aplana
|
||||
coincideixi
|
||||
coma
|
||||
comes
|
||||
comprova
|
||||
comprovar
|
||||
comprovar-ho
|
||||
comuna
|
||||
convidarem
|
||||
estarem
|
||||
existeixen
|
||||
existeixi
|
||||
fase
|
||||
gratuïta
|
||||
interna
|
||||
mateixa
|
||||
meves
|
||||
molta
|
||||
oberta
|
||||
obertes
|
||||
permeten
|
||||
permeti
|
||||
segura
|
||||
targeta
|
||||
teus
|
||||
usa
|
||||
usen
|
||||
niar
|
||||
negreta
|
||||
emplena
|
||||
emplenament
|
||||
edita
|
||||
selector
|
||||
atributs
|
||||
sobreescriuran
|
||||
compartides
|
||||
desenvolupadors
|
||||
administradors
|
||||
desbloquejar
|
||||
desbloquegeu
|
||||
opcionalment
|
||||
autoreferència
|
||||
especificant
|
||||
previsualització
|
||||
previsualitza
|
||||
predeterminat
|
||||
predeterminats
|
||||
predeterminada
|
||||
predefinides
|
||||
predefinida
|
||||
desactivades
|
||||
seleccionada
|
||||
seleccionades
|
||||
descarregueu
|
||||
centralitzat
|
||||
horitzontalment
|
||||
verticalment
|
||||
multijugador
|
||||
multiorganització
|
||||
complementari
|
||||
multiplicador
|
||||
actualitzarà
|
||||
activades
|
||||
activador
|
||||
cancel·laran
|
||||
comptaran
|
||||
configurar
|
||||
confirmar-ho
|
||||
conservaran
|
||||
desagrupa
|
||||
desbloqueja
|
||||
desenganxa
|
||||
despublica
|
||||
despublicar
|
||||
desselecciona-ho
|
||||
duplicades
|
||||
envia-ho
|
||||
importada
|
||||
importades
|
||||
interactius
|
||||
intercanvia
|
||||
lliscament
|
||||
lliuraran
|
||||
migracions
|
||||
mixte
|
||||
personalitzeu-les
|
||||
plantilles
|
||||
privadesa
|
||||
realitzant
|
||||
repositori
|
||||
restauraran
|
||||
selecciona-ho
|
||||
superposa
|
||||
transferiu
|
||||
unir-se
|
||||
usar-los
|
||||
autodesat
|
||||
demanar-ho
|
||||
desactivar-los
|
||||
abandonar-la
|
||||
accedir-hi
|
||||
accelereu
|
||||
activar-los
|
||||
actualitzeu
|
||||
adoptarà
|
||||
afegir-hi
|
||||
afegir-ne
|
||||
afegiu
|
||||
afegiu-ne
|
||||
agrupant-la
|
||||
ajudar-nos
|
||||
ajudar-vos
|
||||
ajudeu-nos
|
||||
ajudieu-nos
|
||||
ajustar-lo
|
||||
ajusteu
|
||||
ajustis
|
||||
alineació
|
||||
amplia
|
||||
ampliada
|
||||
amplieu
|
||||
animacions
|
||||
aplicant
|
||||
apliqueu
|
||||
arrossegueu
|
||||
assegurar-vos
|
||||
avançar-vos
|
||||
avisos
|
||||
baixades
|
||||
cancel·lada
|
||||
cancel·lant
|
||||
cercador
|
||||
coincidents
|
||||
col·laboració
|
||||
col·laborar
|
||||
commuteu
|
||||
compatibilitat
|
||||
concedir-hi
|
||||
consumeixin
|
||||
conèixer-vos
|
||||
definir-ne
|
||||
definiu
|
||||
depuració
|
||||
desbloca
|
||||
descarta-ho
|
||||
descriviu
|
||||
desemmascara
|
||||
deshabilita
|
||||
deshabilitada
|
||||
deshabilitades
|
||||
desplaçar
|
||||
desvincula
|
||||
dissenyeu
|
||||
editar-lo
|
||||
editen
|
||||
editeu
|
||||
eliminar-lo
|
||||
el·lipse
|
||||
el·lipses
|
||||
emplenats
|
||||
encabir-ho
|
||||
enfosqueix
|
||||
escriviu-nos
|
||||
espaiat
|
||||
espaiats
|
||||
especifiqueu
|
||||
estils
|
||||
expliqueu-nos
|
||||
exporta
|
||||
exportant
|
||||
exportar
|
||||
flexibilitat
|
||||
gaudireu
|
||||
il·limitat
|
||||
il·limitats
|
||||
il·lumina
|
||||
il·lustracions
|
||||
importats
|
||||
incrusta
|
||||
inhabilitada
|
||||
inicieu
|
||||
insereix
|
||||
inspecciona
|
||||
inspeccionar-ne
|
||||
instal·la
|
||||
instal·lació
|
||||
instal·lada
|
||||
instal·lades
|
||||
instal·lat
|
||||
instantànies
|
||||
integracions
|
||||
intentar-ho
|
||||
marca-ho
|
||||
migració
|
||||
milloreu
|
||||
milloreu-ho
|
||||
notificacions
|
||||
obsolet
|
||||
obteniu
|
||||
ometeu-ho
|
||||
omplir-ho
|
||||
opacitat
|
||||
orientar-vos
|
||||
personalitzada
|
||||
personalitzades
|
||||
pestanya
|
||||
porta-ho
|
||||
porta-retalls
|
||||
promoveu
|
||||
prototipar
|
||||
prototipatge
|
||||
publicar-la
|
||||
reassignar
|
||||
reassigneu
|
||||
remapejant
|
||||
remapeja
|
||||
remapejar
|
||||
silenciats
|
||||
sol·licitat
|
||||
sol·licitud
|
||||
sol·licituds
|
||||
suprimiran
|
||||
tipogràfic
|
||||
tipogràfica
|
||||
tipogràfics
|
||||
tipogràfiques
|
||||
torna-ho
|
||||
valorant
|
||||
vinculades
|
||||
vincular
|
||||
visiteu
|
||||
volteja
|
||||
unir-s'hi
|
||||
l'opacitat
|
||||
l'espaiat
|
||||
l'interlineat
|
||||
explora'n
|
||||
edita'l
|
||||
desant
|
||||
aquesta
|
||||
aquestes
|
||||
baixa
|
||||
capa
|
||||
capes
|
||||
compartir
|
||||
comproveu
|
||||
desa
|
||||
deseu
|
||||
dreta
|
||||
esteu
|
||||
files
|
||||
fixa
|
||||
inicia
|
||||
inicials
|
||||
meus
|
||||
només
|
||||
nous
|
||||
noves
|
||||
pel
|
||||
perfil
|
||||
perquè
|
||||
pes
|
||||
pla
|
||||
seus
|
||||
totes
|
||||
treballo
|
||||
una
|
||||
vosaltres
|
||||
vàlida
|
||||
vàlides
|
||||
desactiva
|
||||
desactivat
|
||||
desactivada
|
||||
desconnecta
|
||||
descobreix
|
||||
connectades
|
||||
alguna
|
||||
canvia
|
||||
compost
|
||||
comprovant
|
||||
del
|
||||
dels
|
||||
des
|
||||
enviarem
|
||||
mixtes
|
||||
noms
|
||||
quina
|
||||
senzilla
|
||||
uneix
|
||||
ves
|
||||
vista
|
||||
vés
|
||||
desconegut
|
||||
fixa
|
||||
pel
|
||||
pels
|
||||
tota
|
||||
comentari
|
||||
comunitat
|
||||
set
|
||||
desvinculat
|
||||
separades
|
||||
|
||||
[brands]
|
||||
321
frontend/scripts/check-translations/words.es.txt
Normal file
321
frontend/scripts/check-translations/words.es.txt
Normal file
@ -0,0 +1,321 @@
|
||||
# Word catalog for check-translations.js (Spanish).
|
||||
# Comment lines start with #. Blank lines are ignored.
|
||||
# [function] short function words; a token starting with one of
|
||||
# these is likely missing a space.
|
||||
# [common] ordinary words used to judge the other half of a split.
|
||||
# [ok] correct words the splitter would flag; never report
|
||||
# these. Add new valid words here when the gate trips.
|
||||
# [brands] extra locale-specific brands/terms kept as-is.
|
||||
# [elision] letters elided with an apostrophe (Spanish has none).
|
||||
|
||||
[elision]
|
||||
|
||||
[function]
|
||||
el
|
||||
la
|
||||
los
|
||||
las
|
||||
un
|
||||
una
|
||||
unos
|
||||
unas
|
||||
al
|
||||
del
|
||||
de
|
||||
en
|
||||
y
|
||||
e
|
||||
o
|
||||
u
|
||||
que
|
||||
como
|
||||
con
|
||||
por
|
||||
para
|
||||
sin
|
||||
sobre
|
||||
entre
|
||||
hasta
|
||||
desde
|
||||
donde
|
||||
cuando
|
||||
porque
|
||||
pues
|
||||
si
|
||||
no
|
||||
ni
|
||||
mas
|
||||
más
|
||||
muy
|
||||
tan
|
||||
tanto
|
||||
todo
|
||||
todos
|
||||
toda
|
||||
todas
|
||||
cada
|
||||
otro
|
||||
otra
|
||||
otros
|
||||
otras
|
||||
este
|
||||
esta
|
||||
estos
|
||||
estas
|
||||
ese
|
||||
esa
|
||||
eso
|
||||
aquel
|
||||
aquella
|
||||
aquello
|
||||
mi
|
||||
mis
|
||||
tu
|
||||
tus
|
||||
su
|
||||
sus
|
||||
nuestro
|
||||
nuestra
|
||||
nuestros
|
||||
nuestras
|
||||
vuestro
|
||||
vuestra
|
||||
vuestros
|
||||
vuestras
|
||||
me
|
||||
te
|
||||
se
|
||||
nos
|
||||
os
|
||||
le
|
||||
les
|
||||
lo
|
||||
yo
|
||||
tú
|
||||
él
|
||||
ella
|
||||
ello
|
||||
nosotros
|
||||
vosotros
|
||||
ellos
|
||||
ellas
|
||||
esto
|
||||
eso
|
||||
aquello
|
||||
algo
|
||||
alguien
|
||||
nada
|
||||
nadie
|
||||
quien
|
||||
quienes
|
||||
cual
|
||||
cuales
|
||||
cuanto
|
||||
cuanta
|
||||
cuantos
|
||||
cuantas
|
||||
|
||||
[common]
|
||||
habilitar
|
||||
crear
|
||||
eliminar
|
||||
guardar
|
||||
abrir
|
||||
cerrar
|
||||
enviar
|
||||
compartir
|
||||
invitar
|
||||
mover
|
||||
copiar
|
||||
pegar
|
||||
subir
|
||||
bajar
|
||||
editar
|
||||
configurar
|
||||
activar
|
||||
desactivar
|
||||
permitir
|
||||
necesitar
|
||||
poder
|
||||
deber
|
||||
querer
|
||||
saber
|
||||
hacer
|
||||
poner
|
||||
salir
|
||||
entrar
|
||||
volver
|
||||
llegar
|
||||
pasar
|
||||
quedar
|
||||
dar
|
||||
estar
|
||||
ser
|
||||
haber
|
||||
tener
|
||||
ir
|
||||
ver
|
||||
usar
|
||||
buscar
|
||||
cambiar
|
||||
seguir
|
||||
encontrar
|
||||
mostrar
|
||||
ocultar
|
||||
añadir
|
||||
quitar
|
||||
seleccionar
|
||||
aplicar
|
||||
cancelar
|
||||
aceptar
|
||||
probar
|
||||
comprobar
|
||||
generar
|
||||
descargar
|
||||
equipo
|
||||
archivo
|
||||
proyecto
|
||||
cuenta
|
||||
nombre
|
||||
correo
|
||||
error
|
||||
aviso
|
||||
ventana
|
||||
página
|
||||
botón
|
||||
campo
|
||||
lista
|
||||
tabla
|
||||
imagen
|
||||
forma
|
||||
capa
|
||||
color
|
||||
tamaño
|
||||
fecha
|
||||
enlace
|
||||
nuevo
|
||||
nueva
|
||||
grande
|
||||
pequeño
|
||||
bueno
|
||||
actual
|
||||
siguiente
|
||||
anterior
|
||||
mismo
|
||||
otra
|
||||
primer
|
||||
último
|
||||
único
|
||||
libre
|
||||
público
|
||||
|
||||
[ok]
|
||||
abandonarla
|
||||
abrirlo
|
||||
activarlos
|
||||
actuales
|
||||
actualizada
|
||||
adicionales
|
||||
administradores
|
||||
algunos
|
||||
alto
|
||||
aplicarse
|
||||
asignarse
|
||||
aun
|
||||
aunque
|
||||
autoguardada
|
||||
autoguardado
|
||||
autoreferencia
|
||||
ayudarnos
|
||||
ayudarte
|
||||
borradores
|
||||
borrarlo
|
||||
cambiada
|
||||
caracteres
|
||||
como
|
||||
comparte
|
||||
compartir
|
||||
comprobar
|
||||
comprueba
|
||||
compuesto
|
||||
comunidad
|
||||
conectarlo
|
||||
conectarse
|
||||
conjunto
|
||||
conocerte
|
||||
consigue
|
||||
contactarnos
|
||||
contener
|
||||
contiene
|
||||
contexto
|
||||
danos
|
||||
demás
|
||||
desactivarlos
|
||||
deseleccionar
|
||||
desuscripción
|
||||
durante
|
||||
editores
|
||||
eje
|
||||
eliminarlo
|
||||
eliminarlos
|
||||
ella
|
||||
eso
|
||||
esos
|
||||
este
|
||||
estoy
|
||||
estándar
|
||||
expirada
|
||||
funcionalidades
|
||||
granulares
|
||||
hay
|
||||
hubo
|
||||
importada
|
||||
iniciarse
|
||||
invitaciones
|
||||
lectores
|
||||
letras
|
||||
manifiesto
|
||||
menos
|
||||
nosotros
|
||||
otras
|
||||
pero
|
||||
personales
|
||||
peso
|
||||
primero
|
||||
principales
|
||||
propiedades
|
||||
proveedores
|
||||
publicarla
|
||||
publicarlo
|
||||
puesto
|
||||
regenerada
|
||||
restaurarlo
|
||||
restaurarlos
|
||||
seleccionada
|
||||
selector
|
||||
servidores
|
||||
sino
|
||||
solicitudes
|
||||
soportada
|
||||
sufijo
|
||||
tanto
|
||||
temas
|
||||
tenerte
|
||||
tipográfico
|
||||
tono
|
||||
unidades
|
||||
unir
|
||||
unirse
|
||||
uno
|
||||
unos
|
||||
usarlas
|
||||
utilizada
|
||||
verla
|
||||
versiones
|
||||
verticales
|
||||
vincularse
|
||||
visuales
|
||||
volverlo
|
||||
porque
|
||||
|
||||
[brands]
|
||||
@ -1,11 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import getopts from "getopts";
|
||||
import { promises as fs, createReadStream } from "fs";
|
||||
import { promises as fs, createReadStream } from "node:fs";
|
||||
import gt from "gettext-parser";
|
||||
import l from "lodash";
|
||||
import path from "path";
|
||||
import readline from "readline";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
|
||||
const baseLocale = "en";
|
||||
|
||||
@ -24,18 +23,6 @@ async function* getFiles(dir) {
|
||||
}
|
||||
}
|
||||
|
||||
async function translationExists(locale) {
|
||||
const target = path.normalize("./translations/");
|
||||
const targetPath = path.join(target, `${locale}.po`);
|
||||
|
||||
try {
|
||||
const result = await fs.stat(targetPath);
|
||||
return true;
|
||||
} catch (cause) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function readLocaleByPath(path) {
|
||||
const content = await fs.readFile(path);
|
||||
return gt.po.parse(content, "utf-8");
|
||||
@ -78,7 +65,7 @@ async function processLocale(options, f) {
|
||||
} else if (locales === undefined) {
|
||||
} else {
|
||||
console.error(`Invalid value found on locales parameter: '${locales}'`);
|
||||
process.exit(-1);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
for await (const { name } of scanLocales()) {
|
||||
@ -336,42 +323,513 @@ async function synchronize(options, ...other) {
|
||||
});
|
||||
}
|
||||
|
||||
// Brands kept as-is in every locale.
|
||||
const GENERIC_BRANDS = [
|
||||
"GitHub",
|
||||
"GitLab",
|
||||
"YouTube",
|
||||
"InVision",
|
||||
"innerShadow",
|
||||
"dropShadow",
|
||||
"iOS",
|
||||
"macOS",
|
||||
];
|
||||
|
||||
const TOKEN_RE = /[\p{L}\p{M}]+(?:[·'’\-][\p{L}\p{M}]+)*/gu;
|
||||
const SKIP_RE = /[%{@/\\=<>|#0-9]/;
|
||||
const PLACEHOLDER_RES = [/%[sd]/g, /\{[^}]*\}/g, /%\([^)]*\)[sd]/g];
|
||||
const PUNCT_RE = /[,.:;!?…»)\]]([A-Za-zÀ-Úà-ú«("“‘$])/gu;
|
||||
const CAMEL_RE = /[a-zàèéíòóúüç·]([A-ZÀÈÉÍÒÓÚÜ][a-zàèéíòóúü]+)/gu;
|
||||
const PH_GLUED_RE = /%[sd](?=[A-Za-zÀ-Úà-ú])/gu;
|
||||
const APOSTROPHE_DIGIT_RE = /[a-zàèéíòóúüç·]d['’][0-9]/gu;
|
||||
|
||||
function tokenize(text) {
|
||||
return [...text.matchAll(TOKEN_RE)].map((m) => m[0].toLowerCase());
|
||||
}
|
||||
|
||||
function elisionBase(token, elision) {
|
||||
if (!elision) return null;
|
||||
const m = token.match(new RegExp(`^[${elision}]['’](.+)$`));
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
function countIn(text, re) {
|
||||
re.lastIndex = 0;
|
||||
return [...text.matchAll(re)].length;
|
||||
}
|
||||
|
||||
// A word is valid as one half of a split. Never the whole token:
|
||||
// a repeated glue (`del'equip` x3) must not validate itself by frequency.
|
||||
function validPart(word, words, freq) {
|
||||
return (
|
||||
words.functionWords.has(word) ||
|
||||
words.okWords.has(word) ||
|
||||
words.commonWords.has(word) ||
|
||||
(freq.get(word) ?? 0) >= 1
|
||||
);
|
||||
}
|
||||
|
||||
// Every (left, right, rule) split of a token, including each
|
||||
// hyphen-separated segment.
|
||||
function* splits(tok, words, freq) {
|
||||
const cands = [tok, ...tok.split("-")];
|
||||
const seen = new Set();
|
||||
for (const c of cands) {
|
||||
if (c.length < 3) continue;
|
||||
for (let k = 1; k < c.length; k++) {
|
||||
const left = c.slice(0, k);
|
||||
const right = c.slice(k);
|
||||
if (left.length < 1) continue;
|
||||
if (right.length < 2 && !words.functionWords.has(right)) continue;
|
||||
const key = left + "|" + right;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const rightBase = elisionBase(right, words.elision) ?? right;
|
||||
const rightOk = validPart(rightBase, words, freq);
|
||||
const leftOk = validPart(left, words, freq);
|
||||
if (words.functionWords.has(left) && rightOk)
|
||||
yield [left, right, "func-left"];
|
||||
else if (words.functionWords.has(right) && right.length <= 4 && leftOk)
|
||||
yield [left, right, "func-right"];
|
||||
else if (
|
||||
c.length >= 8 &&
|
||||
left.length >= 3 &&
|
||||
right.length >= 2 &&
|
||||
leftOk &&
|
||||
rightOk
|
||||
)
|
||||
yield [left, right, "content"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const RULE_ORDER = { "func-left": 0, "func-right": 1, content: 2 };
|
||||
|
||||
function bestSplit(tok, words, freq) {
|
||||
let best = null;
|
||||
for (const [left, right, rule] of splits(tok, words, freq)) {
|
||||
if (left.includes("-")) continue;
|
||||
if (
|
||||
!best ||
|
||||
RULE_ORDER[rule] < RULE_ORDER[best[2]] ||
|
||||
(RULE_ORDER[rule] === RULE_ORDER[best[2]] && left.length > best[0].length)
|
||||
) {
|
||||
best = [left, right, rule];
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function folded(s) {
|
||||
return s.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
||||
}
|
||||
|
||||
function distance(a, b) {
|
||||
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i]);
|
||||
for (let j = 1; j <= b.length; j++) dp[0][j] = j;
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
dp[i][j] = Math.min(
|
||||
dp[i - 1][j] + 1,
|
||||
dp[i][j - 1] + 1,
|
||||
dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
return dp[a.length][b.length];
|
||||
}
|
||||
|
||||
// Resemblance to the source: a correct word almost always resembles
|
||||
// its cognate in `en`/`es`; a glued one never does. A pure affix
|
||||
// (1-4 letters more or less) doesn't count: that is exactly the
|
||||
// shape of a glue (`lapolitica` vs `politica`, `desdel` vs `desde`).
|
||||
function isCognate(tok, refToks, elision) {
|
||||
const base = tok.replace(new RegExp(`^[${elision || "-"}]['’]`), "");
|
||||
const t = folded(base);
|
||||
const limit = t.length <= 4 ? 0 : 1;
|
||||
for (const r of refToks) {
|
||||
const rt = folded(r);
|
||||
if (Math.abs(rt.length - t.length) > Math.max(limit, 4)) continue;
|
||||
const d = distance(t, rt);
|
||||
if (d === 0) return true;
|
||||
if (d > limit) continue;
|
||||
if (isAffix(t, rt)) continue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isAffix(t, rt) {
|
||||
const dif = Math.abs(t.length - rt.length);
|
||||
if (dif < 1 || dif > 4) return false;
|
||||
return (
|
||||
t.startsWith(rt) || t.endsWith(rt) || rt.startsWith(t) || rt.endsWith(t)
|
||||
);
|
||||
}
|
||||
|
||||
function refToks(...texts) {
|
||||
const toks = new Set();
|
||||
for (const text of texts) for (const t of tokenize(text)) toks.add(t);
|
||||
return toks;
|
||||
}
|
||||
|
||||
function cleanContext(frag) {
|
||||
return !/https?:|www\.|@|\|target:|\.mcp\.json/.test(frag);
|
||||
}
|
||||
|
||||
function checkPunctuation(text, brands) {
|
||||
const found = [];
|
||||
for (const m of text.matchAll(PUNCT_RE)) {
|
||||
const punct = m[0][0];
|
||||
const next = m[1];
|
||||
const frag = text.slice(Math.max(0, m.index - 30), m.index + 32);
|
||||
if (!cleanContext(frag)) continue;
|
||||
if (m[0] === "](") continue; // markdown link
|
||||
if (m[0] === ":s" && frag.includes("|target:")) continue; // [x|target:self]
|
||||
if (/%[sd]\.\(/.test(frag)) continue; // %s.(suffix)... notation
|
||||
if (punct === "." && !/[A-ZÀÈÉÍÒÓÚÜ«"“(%$]/.test(next)) continue;
|
||||
found.push({ what: m[0], frag });
|
||||
}
|
||||
for (const m of text.matchAll(CAMEL_RE)) {
|
||||
const frag = text.slice(Math.max(0, m.index - 30), m.index + 32);
|
||||
if (brands.some((mk) => frag.includes(mk))) continue;
|
||||
found.push({ what: m[0], frag });
|
||||
}
|
||||
for (const m of text.matchAll(PH_GLUED_RE)) {
|
||||
const frag = text.slice(Math.max(0, m.index - 30), m.index + 32);
|
||||
found.push({ what: m[0], frag });
|
||||
}
|
||||
for (const m of text.matchAll(APOSTROPHE_DIGIT_RE)) {
|
||||
const frag = text.slice(Math.max(0, m.index - 30), m.index + 32);
|
||||
found.push({ what: m[0], frag });
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function loadFile(path) {
|
||||
return fs.readFile(path).then((buf) => gt.po.parse(buf, "utf-8"));
|
||||
}
|
||||
|
||||
function parseCatalog(text, locale) {
|
||||
const words = {
|
||||
functionWords: new Set(),
|
||||
commonWords: new Set(),
|
||||
okWords: new Set(),
|
||||
brands: [],
|
||||
elision: "",
|
||||
};
|
||||
const sections = { function: 1, common: 1, ok: 1, brands: 1, elision: 1 };
|
||||
let current = null;
|
||||
for (const raw of text.split("\n")) {
|
||||
const line = raw.trim();
|
||||
if (line === "" || line.startsWith("#")) continue;
|
||||
const sec = line.match(/^\[([a-z]+)\]$/);
|
||||
if (sec) {
|
||||
if (!sections[sec[1]]) {
|
||||
throw new Error(`words.${locale}.txt: unknown section [${sec[1]}]`);
|
||||
}
|
||||
current = sec[1];
|
||||
continue;
|
||||
}
|
||||
if (!current) {
|
||||
throw new Error(`words.${locale}.txt: word outside any section`);
|
||||
}
|
||||
if (current === "elision") {
|
||||
if (!/^[a-z]+$/.test(line)) {
|
||||
throw new Error(`words.${locale}.txt: bad [elision] line`);
|
||||
}
|
||||
words.elision += line;
|
||||
} else if (current === "brands") {
|
||||
words.brands.push(line);
|
||||
} else {
|
||||
words[`${current}Words`].add(line.toLowerCase());
|
||||
}
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
async function loadCatalog(locale) {
|
||||
const path = `./scripts/check-translations/words.${locale}.txt`;
|
||||
try {
|
||||
return parseCatalog(await fs.readFile(path, "utf-8"), locale);
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function isFuzzy(entry) {
|
||||
return (entry.comments?.flag ?? "").split(/,\s*/).includes("fuzzy");
|
||||
}
|
||||
|
||||
async function check(locale, words) {
|
||||
const [data, dataEn, dataEs] = await Promise.all([
|
||||
loadFile(`./translations/${locale}.po`),
|
||||
loadFile("./translations/en.po"),
|
||||
locale === "es" ? null : loadFile("./translations/es.po").catch(() => null),
|
||||
]);
|
||||
const entries = data.translations[""];
|
||||
const entriesEn = dataEn.translations[""];
|
||||
const entriesEs = dataEs ? dataEs.translations[""] : {};
|
||||
const brands = [...GENERIC_BRANDS, ...(words?.brands ?? [])];
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
const freq = new Map();
|
||||
for (const [msgid, e] of Object.entries(entries)) {
|
||||
if (msgid === "" || isFuzzy(e)) continue;
|
||||
for (const t of e.msgstr)
|
||||
for (const w of tokenize(t)) {
|
||||
freq.set(w, (freq.get(w) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [msgid, e] of Object.entries(entries)) {
|
||||
if (msgid === "" || isFuzzy(e)) continue;
|
||||
const texts = e.msgstr;
|
||||
const eEn = entriesEn[msgid];
|
||||
const textsEn = eEn ? eEn.msgstr : [];
|
||||
const eEs = entriesEs[msgid];
|
||||
const textsEs = eEs ? eEs.msgstr : [];
|
||||
|
||||
if (eEn?.msgid_plural && !e.msgid_plural) {
|
||||
errors.push(
|
||||
`${msgid}: source uses msgid_plural but ${locale} lacks msgstr[0]/[1]`,
|
||||
);
|
||||
}
|
||||
|
||||
texts.forEach((text, i) => {
|
||||
if (!text) return;
|
||||
const textEn = textsEn[i] ?? "";
|
||||
const textEs = textsEs[i] ?? "";
|
||||
const refs = refToks(textEn, textEs);
|
||||
if (textEn) {
|
||||
const unused = (e.comments?.flag ?? "")
|
||||
.split(/,\s*/)
|
||||
.includes("unused");
|
||||
for (const re of PLACEHOLDER_RES) {
|
||||
const nEn = countIn(textEn, re);
|
||||
const nLoc = countIn(text, re);
|
||||
if (nEn !== nLoc) {
|
||||
// Unused keys are never rendered: report, don't fail.
|
||||
// "Fixing" them by deleting placeholders can destroy
|
||||
// content that a reactivation may need.
|
||||
(unused ? warnings : errors).push(
|
||||
`${msgid}[${i}]: placeholder mismatch ${re.source}: en=${nEn} ${locale}=${nLoc}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const t of checkPunctuation(text, brands)) {
|
||||
errors.push(
|
||||
`${msgid}: glued punctuation ${JSON.stringify(t.what)} ...${t.frag}...`,
|
||||
);
|
||||
}
|
||||
if (!words) return;
|
||||
for (const tok of tokenize(text)) {
|
||||
if (SKIP_RE.test(tok)) continue;
|
||||
if (words.okWords.has(tok)) continue;
|
||||
// No whole-token frequency skip: a repeated glue
|
||||
// (`del'equip` x3) must never validate itself by frequency.
|
||||
const base = elisionBase(tok, words.elision);
|
||||
if (base && validPart(base, words, freq)) continue;
|
||||
if (isCognate(tok, refs, words.elision)) continue;
|
||||
const part = bestSplit(tok, words, freq);
|
||||
if (!part) continue;
|
||||
const [left, right, rule] = part;
|
||||
const line = `${msgid}: glued word ${JSON.stringify(tok)} -> ${left} + ${right}`;
|
||||
if (rule === "content") warnings.push(`${line} (review)`);
|
||||
else errors.push(line);
|
||||
}
|
||||
});
|
||||
}
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
const FIXTURES = [
|
||||
// [text, expectsError]
|
||||
["Els membres del'equip continuaran.", true],
|
||||
["Accepteu lapolítica de privadesa.", true],
|
||||
["Si necessiteu més informació,contacteu amb nosaltres.", true],
|
||||
["Revisions delPenpot disponibles.", true],
|
||||
["Seleccioneu Lowercase oCapitalize.", true],
|
||||
["Cobreix fins a %seditors nous.", true],
|
||||
["Bienvenido acasa nueva.", true],
|
||||
["Les biblioteques compartides.", false],
|
||||
["Edita el webhook.", false],
|
||||
["Emplenament del grup.", false],
|
||||
["S'està desant el fitxer.", false],
|
||||
["Els components no es poden niar.", false],
|
||||
["Gira horitzontalment.", false],
|
||||
["Desbloquegeu les funcions.", false],
|
||||
["Atributs SVG importats.", false],
|
||||
["Commuta la negreta.", false],
|
||||
["Bibliotecas compartidas.", false],
|
||||
];
|
||||
|
||||
async function selfTest(catalog) {
|
||||
// Minimal vocabulary for the fixtures.
|
||||
const freq = new Map(
|
||||
"els membres continuaran accepteu de privadesa si necessiteu més informació amb nosaltres revisions disponibles seleccioneu lowercase infrequent les biblioteques compartides edita el webhook emplenament del grup està desant fitxer components no es poden niar gira commuta la negreta desbloquegeu les funcions atributs svg importats horitzontalment podeu crear equip política casa bibliotecas compartidas bienvenido nueva"
|
||||
.split(" ")
|
||||
.map((w) => [w, 3]),
|
||||
);
|
||||
const words = {
|
||||
functionWords: catalog.functionWords,
|
||||
commonWords: new Set(),
|
||||
okWords: new Set(),
|
||||
brands: [],
|
||||
elision: catalog.elision,
|
||||
};
|
||||
let bad = 0;
|
||||
for (const [text, expectsError] of FIXTURES) {
|
||||
const found = [];
|
||||
for (const t of checkPunctuation(text, GENERIC_BRANDS))
|
||||
found.push(`punct:${t.what}`);
|
||||
for (const tok of tokenize(text)) {
|
||||
if (SKIP_RE.test(tok) || words.functionWords.has(tok)) continue;
|
||||
if (words.okWords.has(tok)) continue;
|
||||
const base = elisionBase(tok, words.elision);
|
||||
if (base && validPart(base, words, freq)) continue;
|
||||
const part = bestSplit(tok, words, freq);
|
||||
if (part && part[2] !== "content") found.push(`tok:${tok}`);
|
||||
}
|
||||
if (found.length > 0 !== expectsError) {
|
||||
console.error(
|
||||
`SELF-TEST FAILED: ${JSON.stringify(text)} expected error=${expectsError}, found=${JSON.stringify(found)}`,
|
||||
);
|
||||
bad++;
|
||||
}
|
||||
}
|
||||
if (bad > 0) process.exit(1);
|
||||
console.log(`SELF-TEST OK: ${FIXTURES.length} cases`);
|
||||
}
|
||||
|
||||
const HELP_TOP = `PO translation toolkit.
|
||||
Usage: node ./scripts/translations.js <command> [options]
|
||||
|
||||
Available options (work before or after the command):
|
||||
|
||||
--locale -l <locale> : restrict the command to one locale
|
||||
--verbose -v : verbose output
|
||||
--help -h : this help, or help for <command>
|
||||
|
||||
Available subcommands (run from \`frontend/\`):
|
||||
`;
|
||||
|
||||
const COMMANDS = {
|
||||
rehash: {
|
||||
args: "",
|
||||
help: 'Scan ./src for (tr "key") usages and update en.po references.',
|
||||
run: (options, params) => rehash(options, ...params),
|
||||
},
|
||||
sync: {
|
||||
args: "[-l <locale>]",
|
||||
help: "Copy #: references and flags from en.po into each locale.",
|
||||
run: (options, params) => synchronize(options, ...params),
|
||||
},
|
||||
delete: {
|
||||
args: "<prefix> [-l <locale>]",
|
||||
help: "Delete every entry whose key starts with <prefix>.",
|
||||
run: (options, params) => {
|
||||
if (!params[0]) {
|
||||
console.error("delete needs a <prefix>");
|
||||
process.exit(2);
|
||||
}
|
||||
return deleteByPrefix(options, ...params);
|
||||
},
|
||||
},
|
||||
fuzzy: {
|
||||
args: "<prefix> [-l <locale>]",
|
||||
help: "Mark as fuzzy every entry whose key starts with <prefix>.",
|
||||
run: (options, params) => {
|
||||
if (!params[0]) {
|
||||
console.error("fuzzy needs a <prefix>");
|
||||
process.exit(2);
|
||||
}
|
||||
return markFuzzy(options, ...params);
|
||||
},
|
||||
},
|
||||
check: {
|
||||
args: "-l <locale> [--self-test]",
|
||||
help: "QA a locale PO: glued words, placeholders, plurals.",
|
||||
run: async (options, params) => {
|
||||
if (options["self-test"]) {
|
||||
const catalog = await loadCatalog("ca");
|
||||
if (!catalog) {
|
||||
console.error("SELF-TEST FAILED: cannot load words.ca.txt");
|
||||
process.exit(2);
|
||||
}
|
||||
await selfTest(catalog);
|
||||
return;
|
||||
}
|
||||
const locale = options.l ?? options.locale;
|
||||
if (!locale) {
|
||||
console.error("check needs -l <locale>");
|
||||
process.exit(2);
|
||||
}
|
||||
const catalog = await loadCatalog(locale);
|
||||
if (!catalog) {
|
||||
console.log(
|
||||
`note: no word catalog for '${locale}', lexical checks skipped`,
|
||||
);
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
result = await check(locale, catalog);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Could not read translations/${locale}.po: ${err.message}`,
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
for (const w of result.warnings) console.log(`warn: ${w}`);
|
||||
for (const e of result.errors) console.error(`error: ${e}`);
|
||||
console.log(
|
||||
`${locale}: ${result.errors.length} errors, ${result.warnings.length} warnings`,
|
||||
);
|
||||
process.exit(result.errors.length > 0 ? 1 : 0);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options = getopts(process.argv.slice(2), {
|
||||
boolean: ["h", "v"],
|
||||
boolean: ["h", "v", "self-test"],
|
||||
string: ["l"],
|
||||
alias: {
|
||||
help: ["h"],
|
||||
locale: ["l"],
|
||||
verbose: ["v"],
|
||||
},
|
||||
stopEarly: true,
|
||||
});
|
||||
|
||||
const [command, ...params] = options._;
|
||||
|
||||
if (command === "rehash") {
|
||||
await rehash(options, ...params);
|
||||
} else if (command === "sync") {
|
||||
await synchronize(options, ...params);
|
||||
} else if (command === "delete") {
|
||||
await deleteByPrefix(options, ...params);
|
||||
} else if (command === "fuzzy") {
|
||||
await markFuzzy(options, ...params);
|
||||
} else {
|
||||
console.log(`Translations manipulation script.
|
||||
How to use:
|
||||
./scripts/translation.js <options> <subcommand>
|
||||
|
||||
Available options:
|
||||
|
||||
--locale -l : specify a concrete locale
|
||||
--verbose -v : enables verbose output
|
||||
--help -h : prints this help
|
||||
|
||||
Available subcommands:
|
||||
|
||||
rehash : reads and writes all translations files, sorting and validating
|
||||
sync : synchronize baselocale file with all other locale files
|
||||
delete <prefix> : delete all entries that matches the prefix
|
||||
fuzzy <prefix> : mark as fuzzy all entries that matches the prefix
|
||||
`);
|
||||
function printHelp() {
|
||||
console.log(HELP_TOP);
|
||||
for (const [name, cmd] of Object.entries(COMMANDS)) {
|
||||
console.log(` ${name} ${cmd.args}\n ${cmd.help}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!command || options.h || options.help) {
|
||||
if (command && !COMMANDS[command]) {
|
||||
console.error(`Unknown command '${command}'.`);
|
||||
printHelp();
|
||||
process.exit(2);
|
||||
}
|
||||
if (command) {
|
||||
const cmd = COMMANDS[command];
|
||||
console.log(
|
||||
`Usage: node ./scripts/translations.js ${command} ${cmd.args}\n\n${cmd.help}`,
|
||||
);
|
||||
} else {
|
||||
printHelp();
|
||||
}
|
||||
} else if (!COMMANDS[command]) {
|
||||
console.error(`Unknown command '${command}'.`);
|
||||
printHelp();
|
||||
process.exit(2);
|
||||
} else {
|
||||
await COMMANDS[command].run(options, params);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -112,7 +112,7 @@ msgstr "La contraseña ha sido cambiada"
|
||||
#: src/app/main/ui/auth/recovery_request.cljs:50
|
||||
msgid "auth.notifications.profile-not-verified"
|
||||
msgstr ""
|
||||
"El perfil aun no ha sido verificado, por favor valida el perfil antes de "
|
||||
"El perfil aún no ha sido verificado, por favor valida el perfil antes de "
|
||||
"continuar."
|
||||
|
||||
#: src/app/main/ui/auth/recovery_request.cljs:33
|
||||
@ -1818,7 +1818,7 @@ msgstr "Parece que no es una imagen válida."
|
||||
msgid "errors.member-is-muted"
|
||||
msgstr ""
|
||||
"El perfil que esta invitando tiene los emails silenciados (por reportes de "
|
||||
"spam o alto indice de rebote)."
|
||||
"spam o alto índice de rebote)."
|
||||
|
||||
#: src/app/main/errors.cljs:483
|
||||
msgid "errors.migration-in-progress"
|
||||
@ -1859,7 +1859,7 @@ msgstr "El perfil esta blockeado"
|
||||
#: src/app/main/ui/auth/recovery_request.cljs:53, src/app/main/ui/dashboard/team.cljs:225, src/app/main/ui/dashboard/team.cljs:731, src/app/main/ui/dashboard/team.cljs:1108, src/app/main/ui/onboarding/team_choice.cljs:109, src/app/main/ui/settings/change_email.cljs:33
|
||||
msgid "errors.profile-is-muted"
|
||||
msgstr ""
|
||||
"Tu perfil tiene los emails silenciados (por reportes de spam o alto indice "
|
||||
"Tu perfil tiene los emails silenciados (por reportes de spam o alto índice "
|
||||
"de rebote)."
|
||||
|
||||
#: src/app/main/data/auth.cljs:346, src/app/main/ui/auth/register.cljs:91
|
||||
@ -2642,7 +2642,7 @@ msgstr "Crear nuevo token de acceso"
|
||||
|
||||
#: src/app/main/ui/settings/integrations.cljs:632
|
||||
msgid "integrations.access-tokens.empty.add-one"
|
||||
msgstr "Pulsa el botón \"Crear nuevo token de accesso\" para generar uno."
|
||||
msgstr "Pulsa el botón \"Crear nuevo token de acceso\" para generar uno."
|
||||
|
||||
#: src/app/main/ui/settings/integrations.cljs:631
|
||||
msgid "integrations.access-tokens.empty.no-access-tokens"
|
||||
@ -2655,7 +2655,7 @@ msgstr "Tokens de acceso personales"
|
||||
#: src/app/main/ui/settings/integrations.cljs:611
|
||||
msgid "integrations.access-tokens.personal.description"
|
||||
msgstr ""
|
||||
"Los tokens de accesso personales funcionan como una alternativa a nuestro "
|
||||
"Los tokens de acceso personales funcionan como una alternativa a nuestro "
|
||||
"sistema de autenticación usuario/password y se pueden usar para permitir a "
|
||||
"otras aplicaciones acceso a la API interna de Penpot"
|
||||
|
||||
@ -2665,7 +2665,7 @@ msgstr "Copiar al portapapeles"
|
||||
|
||||
#: src/app/main/ui/settings/integrations.cljs:253
|
||||
msgid "integrations.create-access-token.title"
|
||||
msgstr "Crear token de accesso"
|
||||
msgstr "Crear token de acceso"
|
||||
|
||||
#: src/app/main/ui/settings/integrations.cljs:252
|
||||
msgid "integrations.create-access-token.title.created"
|
||||
@ -8505,19 +8505,19 @@ msgstr "Opciones avanzadas"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:695, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:702
|
||||
msgid "workspace.options.layout-item.layout-item-max-h"
|
||||
msgstr "Altura.Max"
|
||||
msgstr "Altura máx."
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:633, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:640
|
||||
msgid "workspace.options.layout-item.layout-item-max-w"
|
||||
msgstr "Ancho.Max"
|
||||
msgstr "Ancho máx."
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:664, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:672
|
||||
msgid "workspace.options.layout-item.layout-item-min-h"
|
||||
msgstr "Altura.Min"
|
||||
msgstr "Altura mín."
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:600, src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:609
|
||||
msgid "workspace.options.layout-item.layout-item-min-w"
|
||||
msgstr "Ancho.Min"
|
||||
msgstr "Ancho mín."
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs
|
||||
#, unused
|
||||
@ -9785,7 +9785,7 @@ msgstr "Nombre del grupo"
|
||||
#: src/app/main/ui/workspace/tokens/sets.cljs
|
||||
#, unused
|
||||
msgid "workspace.tokens.grouping-set-alert"
|
||||
msgstr "La agrupación de sets aun no está soportada."
|
||||
msgstr "La agrupación de sets aún no está soportada."
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/import/modal.cljs:232
|
||||
msgid "workspace.tokens.import-button-prefix"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user