@proulr/rosetta 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @proulr/rosetta
2
+
3
+ Fonte única de i18n do Proulr (padrão Rosetta): copy de produto para BFFs e app nativo.
4
+
5
+ - **GitHub:** https://github.com/saviorbp/proulr-rosetta
6
+ - **npm:** https://www.npmjs.com/package/@proulr/rosetta
7
+ - **Guia:** [docs/ROSETTA.md](./docs/ROSETTA.md)
8
+ - **CI/CD:** [docs/npm-publish-cicd.md](./docs/npm-publish-cicd.md)
9
+
10
+ ## Formato
11
+
12
+ ```
13
+ src/locales/<locale>/<domain>.json # editar (flat JSON)
14
+ src/generated/locale-messages.ts # gerado — Metro importa só .ts
15
+ ```
16
+
17
+ `pt-BR` = source of truth das keys. Após editar JSON: `npm run locale:generate`.
18
+
19
+ ## Uso rápido
20
+
21
+ ```ts
22
+ import { t, tFormat, resolveLocaleFromQuery } from '@proulr/rosetta';
23
+
24
+ const locale = resolveLocaleFromQuery(ctx.query);
25
+ t('discover.filter.all', locale);
26
+ ```
27
+
28
+ ## Scripts
29
+
30
+ ```bash
31
+ npm run locale:add -- es # novo idioma (JSON vazio + wiring)
32
+ npm run locale:generate # regenera types/catalog
33
+ npm run validate:locales # parity de keys + imports (pre-commit)
34
+ npm run check:locales:strict # sem valores vazios
35
+ npm run typecheck
36
+ ```
37
+
38
+ ## Pre-commit
39
+
40
+ Ao commit, o hook roda `validate:locales`:
41
+
42
+ - Key nova → deve existir em **todos** os idiomas
43
+ - Key removida → deve sumir de **todos** os idiomas
44
+ - Locale novo → todos os domains + `PROULR_LOCALES` + imports em `catalog.ts`
45
+
46
+ ## Regras
47
+
48
+ - BFFs/app só consomem `t()` — zero dicionário local
49
+ - Default / fallback: `pt-BR`
@@ -0,0 +1,247 @@
1
+ # @proulr/rosetta — guia completo
2
+
3
+ Pacote de i18n do Proulr (inspirado na Rosetta da Nubank): **única fonte de copy** para BFFs (wire SDUI) e app nativo (auth, alerts, hybrids).
4
+
5
+ ---
6
+
7
+ ## O que foi feito
8
+
9
+ | Entrega | Detalhe |
10
+ |---------|---------|
11
+ | Pacote `@proulr/rosetta` | `t` / `tFormat` / resolve de locale |
12
+ | Formato JSON por locale | `src/locales/<locale>/<domain>.json` (escala para ~13 idiomas) |
13
+ | `pt-BR` | Fonte das keys; fallback se tradução vazia/ausente |
14
+ | Idiomas iniciais | `pt-BR`, `en` |
15
+ | App | `locale` em todo fetch SDUI; preferência AsyncStorage; auth com globe + tagline ciclando |
16
+ | BFFs | `resolveLocaleFromQuery` + `t()` nos builders BDC |
17
+ | Scripts | `locale:add`, `locale:generate`, `check:locales`, `validate:locales` |
18
+ | Pre-commit | Valida parity de keys + wiring (imports / `PROULR_LOCALES`) |
19
+
20
+ Consumidores (app + BFFs) **não** guardam dicionário de produto — só chamam `t(key, locale)`.
21
+
22
+ ---
23
+
24
+ ## Arquitetura
25
+
26
+ ```
27
+ preferência (system | pt-BR | en | …)
28
+ ↓
29
+ app resolve locale (device / navigator)
30
+ ↓
31
+ fetch SDUI ?locale=…
32
+ ↓
33
+ BFF: t(key, locale) → wire já traduzido
34
+ ↓
35
+ SduiScreenRenderer (não traduz)
36
+
37
+ App nativo (auth/Alert/hybrid): mesmo t() do Rosetta
38
+ ```
39
+
40
+ | Quem | Papel |
41
+ |------|--------|
42
+ | **Rosetta** | Keys + traduções + `t` / `tFormat` |
43
+ | **BFFs** | Traduz na autoria BDC → wire |
44
+ | **App** | Resolve locale; nativo usa `t`; SDUI só renderiza |
45
+ | **SO** | Diálogos de permissão (fora do Rosetta) |
46
+
47
+ ---
48
+
49
+ ## Formato dos arquivos
50
+
51
+ ```
52
+ src/locales/
53
+ pt-BR/ # source of truth (editar aqui)
54
+ common.json
55
+ …
56
+ en/
57
+ …
58
+
59
+ src/generated/
60
+ locale-messages.ts # AUTO-GERADO (Metro-safe) — não editar
61
+ ```
62
+
63
+ Edite só os JSON. Depois rode `npm run locale:generate` (o pre-commit também roda).
64
+
65
+ Cada JSON é **flat** (key → string):
66
+
67
+ ```json
68
+ {
69
+ "auth.login.title": "Bem-vindo de volta",
70
+ "auth.login.submit": "Entrar"
71
+ }
72
+ ```
73
+
74
+ **Não** misturar idiomas no mesmo objeto. Um locale = uma pasta.
75
+
76
+ ---
77
+
78
+ ## Uso no código
79
+
80
+ ### BFF
81
+
82
+ ```ts
83
+ import { t, tFormat, resolveLocaleFromQuery } from '@proulr/rosetta';
84
+
85
+ const locale = resolveLocaleFromQuery(ctx.query);
86
+ title={t('shell.settings.title', locale)}
87
+ subtitle={tFormat('discover.subtitle.huntingIn', locale, { label: city })}
88
+ ```
89
+
90
+ ### App nativo
91
+
92
+ ```ts
93
+ import { t } from '@proulr/rosetta';
94
+ import { getResolvedAppLocale } from '@/lib/app-locale-service';
95
+ // ou
96
+ import { useAppLocale } from '@/hooks/use-app-locale';
97
+
98
+ const { t, locale, setPref } = useAppLocale();
99
+ t('auth.welcome.signIn');
100
+ ```
101
+
102
+ ### Query SDUI
103
+
104
+ O app envia `locale` (resolvido) em todo `buildSduiScreenFetchQuery`. Em settings também envia `app_locale` (preferência: `system` | idioma explícito).
105
+
106
+ ---
107
+
108
+ ## Scripts
109
+
110
+ Rodar **dentro** de `proulr-rosetta/`:
111
+
112
+ | Comando | O que faz |
113
+ |---------|-----------|
114
+ | `npm run locale:add -- es` | Cria pasta do idioma com keys vazias + regenera types/catalog |
115
+ | `npm run locale:add -- es-ES --force` | Idem, sobrescreve templates |
116
+ | `npm run locale:generate` | Regenera `locale-types.ts` + `catalog.ts` a partir de `src/locales` |
117
+ | `npm run check:locales` | Keys de `pt-BR` presentes em todos; vazios = warning |
118
+ | `npm run check:locales:strict` | Idem, mas valor vazio = erro |
119
+ | `npm run validate:locales` | Coverage + wiring (imports, `PROULR_LOCALES`, domains) |
120
+ | `npm run validate:locales:strict` | Validate + sem valores vazios |
121
+ | `npm run typecheck` | `validate:locales` + `tsc` |
122
+ | `npm run lint` | ESLint |
123
+
124
+ ### Exemplos
125
+
126
+ ```bash
127
+ # Novo idioma
128
+ npm run locale:add -- es
129
+ # editar src/locales/es/*.json
130
+ npm run check:locales:strict
131
+
132
+ # Nova key de copy
133
+ # 1. adicionar em src/locales/pt-BR/<domain>.json
134
+ # 2. adicionar a MESMA key em en/ (e demais locales)
135
+ # 3. npm run validate:locales
136
+
137
+ # Remover key
138
+ # apagar a key de TODOS os locales/<id>/<domain>.json
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Pre-commit
144
+
145
+ Hook em `.husky/pre-commit` (requer git no pacote — `git init` se ainda não houver remote):
146
+
147
+ 1. **`npm run locale:generate`** — sincroniza `src/generated/locale-messages.ts` a partir dos JSON
148
+ 2. **`npm run validate:locales`** — falha o commit se:
149
+ - Key nova em `pt-BR` sem a mesma key nos outros idiomas
150
+ - Key removida de `pt-BR` mas ainda presente em outro idioma
151
+ - Key “sobrando” em um locale sem existir em `pt-BR`
152
+ - Pasta de locale sem todos os `*.json` de domínio
153
+ - Locale no disco **sem** estar em `PROULR_LOCALES` / no `locale-messages.ts` gerado
154
+ 3. **`lint-staged`** — ESLint nos arquivos staged
155
+ 4. **`proulr-bump-and-sync-deps`** (se existir no monorepo) — bump de versão publishable
156
+
157
+ ```bash
158
+ cd proulr-rosetta
159
+ npm install # prepare → husky
160
+ npm run validate:locales
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Workflows
166
+
167
+ ### Nova string de produto
168
+
169
+ 1. Escolher domínio (`auth`, `shell`, `discover`, …)
170
+ 2. Adicionar key em **`pt-BR/<domain>.json`**
171
+ 3. Adicionar a **mesma key** em **todos** os outros locales (mesmo que temporariamente com `""` ou já traduzido)
172
+ 4. Usar `t('domain.key', locale)` no BFF ou app
173
+ 5. Commit → pre-commit valida parity
174
+
175
+ ### Novo idioma
176
+
177
+ 1. `npm run locale:add -- <id>` (ex.: `es`, `zh-CN`)
178
+ 2. Traduzir os JSONs gerados
179
+ 3. `npm run check:locales:strict`
180
+ 4. Atualizar UI do app (picker Settings / globe na ante-tela) para incluir o novo locale no ciclo de preferência
181
+ 5. Commit
182
+
183
+ ## Detecção de idioma (app)
184
+
185
+ | Preferência | Comportamento |
186
+ |-------------|---------------|
187
+ | `system` (default) | Mobile: `expo-localization`; Web: `navigator.languages` |
188
+ | `pt-BR` / `en` / … | Força o locale |
189
+
190
+ Troca na ante-tela (globe) ou em Settings → limpa cache SDUI → próximo fetch no novo idioma.
191
+
192
+ ---
193
+
194
+ ## CI/CD (GitHub Actions + npm)
195
+
196
+ Detalhes: [`docs/npm-publish-cicd.md`](./npm-publish-cicd.md).
197
+
198
+ Workflow: [`.github/workflows/publish-npm.yml`](../.github/workflows/publish-npm.yml)
199
+
200
+ | Evento | O que roda |
201
+ |--------|------------|
202
+ | PR / push em `main` (src, scripts, package*) | `npm run typecheck` |
203
+ | Push em `main` após CI ok | `npm publish --access public` via **Trusted Publishing (OIDC)** |
204
+
205
+ ### Setup Trusted Publishing (primeira vez)
206
+
207
+ 1. Em https://www.npmjs.com → scope `@proulr` → criar/`@proulr/rosetta` se precisar
208
+ 2. **Trusted Publisher** → GitHub Actions
209
+ 3. Repository: `saviorbp/proulr-rosetta`
210
+ 4. Workflow filename: `publish-npm.yml`
211
+ 5. Environment: vazio
212
+
213
+ Sem Trusted Publisher, o job `publish` falha (404/401). Depois do publish, consumers usam `"@proulr/rosetta": "^0.1.1"`.
214
+
215
+ ---
216
+
217
+ ## Regras
218
+
219
+ - Identificadores EN; copy via Rosetta; default `pt-BR`
220
+ - **Proibido** literal de produto em BFF/app
221
+ - Nova copy = PR no Rosetta (keys em todos os locales)
222
+ - Renderer SDUI **não** traduz
223
+ - Pre-commit deve passar antes do commit
224
+
225
+ ---
226
+
227
+ ## Estrutura do pacote
228
+
229
+ ```
230
+ proulr-rosetta/
231
+ src/
232
+ locales/ # JSON por locale/domínio
233
+ catalog.ts # merge + getMessage (gerado em parte via locale:generate)
234
+ locale-types.ts # ProulrLocale (gerado via locale:generate)
235
+ translate.ts # t / tFormat
236
+ resolve-locale.ts # query / pref / system tags
237
+ domains.ts
238
+ index.ts
239
+ scripts/
240
+ add-locale.mjs
241
+ generate-locale-wiring.mjs
242
+ check-locale-coverage.mjs
243
+ validate-locales.mjs
244
+ .husky/pre-commit
245
+ docs/ROSETTA.md # este guia
246
+ README.md # resumo + link
247
+ ```
@@ -0,0 +1,50 @@
1
+ # Publicação automática no npm (CI/CD)
2
+
3
+ `@proulr/rosetta` publica no npm quando há push em `main` — mesmo fluxo de `@proulr/sdui-contract` e `@proulr/bff-core`.
4
+
5
+ ## Fluxo
6
+
7
+ 1. Altere o código da lib.
8
+ 2. **Suba a versão** em `package.json` (ex.: `0.1.1` → `0.1.2`).
9
+ 3. Rode `npm install` se mudou dependências (atualiza `package-lock.json`).
10
+ 4. Commit, merge e push em `main`.
11
+ 5. O workflow `.github/workflows/publish-npm.yml` roda typecheck e publica se a versão ainda não existir no npm.
12
+
13
+ Se a versão já estiver publicada, o job **pula** o publish (CI verde, sem erro).
14
+
15
+ ## Autenticação no npm
16
+
17
+ ### Bootstrap (1ª publicação do pacote)
18
+
19
+ OIDC/Trusted Publishing **só funciona depois que o pacote existe** no npm. Igual ao bootstrap de contract/core:
20
+
21
+ 1. Crie um token **Automation** em https://www.npmjs.com/settings/~/tokens (ou reutilize o dos outros `@proulr/*`)
22
+ 2. No GitHub:
23
+
24
+ ```bash
25
+ gh secret set NPM_TOKEN -R saviorbp/proulr-rosetta
26
+ ```
27
+
28
+ 3. Push / re-run do workflow → cria `@proulr/rosetta` no registry
29
+
30
+ ### Trusted Publishing (depois do bootstrap — modo steady-state)
31
+
32
+ 1. https://www.npmjs.com/package/@proulr/rosetta → **Settings** → **Trusted Publisher** → **GitHub Actions**
33
+ 2. Preencha:
34
+
35
+ | Campo | Valor |
36
+ | --- | --- |
37
+ | Organization or user | `saviorbp` |
38
+ | Repository | `proulr-rosetta` |
39
+ | Workflow filename | `publish-npm.yml` |
40
+ | Environment | *(vazio)* |
41
+
42
+ O workflow já tem `id-token: write`. Com Trusted Publisher configurado, o publish usa OIDC (o `NPM_TOKEN` fica só como fallback).
43
+
44
+ **Repositório privado no GitHub:** sem `--provenance`.
45
+
46
+ ## Consumidores (app / BFFs)
47
+
48
+ ```bash
49
+ npm install @proulr/rosetta@^0.1.2
50
+ ```
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@proulr/rosetta",
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "docs"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/saviorbp/proulr-rosetta.git"
18
+ },
19
+ "scripts": {
20
+ "prepare": "husky",
21
+ "locale:add": "node ./scripts/add-locale.mjs",
22
+ "locale:generate": "node ./scripts/generate-locale-wiring.mjs",
23
+ "check:locales": "node ./scripts/check-locale-coverage.mjs",
24
+ "check:locales:strict": "node ./scripts/check-locale-coverage.mjs --strict",
25
+ "validate:locales": "node ./scripts/validate-locales.mjs",
26
+ "validate:locales:strict": "node ./scripts/validate-locales.mjs --strict",
27
+ "typecheck": "npm run locale:generate && npm run validate:locales && tsc --noEmit",
28
+ "prepublishOnly": "npm run typecheck",
29
+ "lint": "eslint .",
30
+ "lint:fix": "eslint . --fix",
31
+ "version:sync": "node ../scripts/proulr-bump-and-sync-deps.mjs"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "devDependencies": {
37
+ "@eslint/js": "^9.39.4",
38
+ "@types/node": "^22.20.0",
39
+ "eslint": "^9.0.0",
40
+ "globals": "^16.0.0",
41
+ "husky": "^9.1.7",
42
+ "lint-staged": "^16.4.0",
43
+ "typescript": "~6.0.3",
44
+ "typescript-eslint": "^8.61.1"
45
+ },
46
+ "engines": {
47
+ "node": ">=22"
48
+ }
49
+ }
package/src/catalog.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { ProulrLocale } from './locale-types';
2
+ import { DEFAULT_PROULR_LOCALE } from './locale-types';
3
+ import { ROSETTA_DOMAINS } from './domains';
4
+ import {
5
+ MESSAGES_BY_LOCALE,
6
+ PT_BR_MESSAGES,
7
+ type FlatMessages,
8
+ type ProulrMessageKey,
9
+ EN_MESSAGES,
10
+ } from './generated/locale-messages';
11
+
12
+ export { PT_BR_MESSAGES, type ProulrMessageKey };
13
+
14
+ /**
15
+ * Legacy nested shape (key → locale → string). Prefer `t(key, locale)`.
16
+ */
17
+ export const PROULR_LOCALE_CATALOG = Object.fromEntries(
18
+ (Object.keys(PT_BR_MESSAGES) as ProulrMessageKey[]).map((key) => [
19
+ key,
20
+ {
21
+ 'pt-BR': PT_BR_MESSAGES[key],
22
+ 'en': EN_MESSAGES[key] ?? PT_BR_MESSAGES[key],
23
+ },
24
+ ]),
25
+ ) as {
26
+ [K in ProulrMessageKey]: Record<ProulrLocale, string>;
27
+ };
28
+
29
+ export function getLocaleMessages(locale: ProulrLocale): FlatMessages {
30
+ return MESSAGES_BY_LOCALE[locale] ?? MESSAGES_BY_LOCALE[DEFAULT_PROULR_LOCALE];
31
+ }
32
+
33
+ export function getMessage(
34
+ key: ProulrMessageKey,
35
+ locale: ProulrLocale = DEFAULT_PROULR_LOCALE,
36
+ ): string {
37
+ const primary = getLocaleMessages(locale)[key];
38
+ if (typeof primary === 'string' && primary.length > 0) return primary;
39
+ return PT_BR_MESSAGES[key] ?? key;
40
+ }
41
+
42
+ export { ROSETTA_DOMAINS };
package/src/domains.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Domain catalog files under `src/locales/<locale>/<domain>.json`.
3
+ * `pt-BR` is the source of truth for message keys.
4
+ */
5
+ export const ROSETTA_DOMAINS = [
6
+ 'common',
7
+ 'shell',
8
+ 'discover',
9
+ 'chat',
10
+ 'flash',
11
+ 'profile',
12
+ 'social',
13
+ 'auth',
14
+ 'alerts',
15
+ 'hybrid',
16
+ ] as const;
17
+
18
+ export type RosettaDomain = (typeof ROSETTA_DOMAINS)[number];