La interfaz está localizada; las descripciones y documentos de las Skills se traducen automáticamente con IA a su idioma. El código, los comandos, rutas y términos técnicos se mantienen en el original, y prevalece el documento del autor.
Explorar Skills
Desarrollo · SKILL.md

backend-internationalization

backend-internationalization

Use this skill when implementing multi-language support, translation workflows, or locale-aware formatting. This skill enforces: BCP 47 locale tags, ICU MessageFormat for complex messages, dot-separated namespaced keys, fallback chains, and CI-validated translation files. Applies to any backend stack serving multi-region users. Do NOT use for: simple string…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentación de la Skill

GitHub

name: backend-internationalization description: > Use this skill when implementing multi-language support, translation workflows, or locale-aware formatting. This skill enforces: BCP 47 locale tags, ICU MessageFormat for complex messages, dot-separated namespaced keys, fallback chains, and CI-validated translation files. Applies to any backend stack serving multi-region users. Do NOT use for: simple string key-value without plural/gender, or single-language applications. version: "2.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [backend, i18n, phase-6, universal]

Backend Internationalization

Purpose

Design i18n architecture with locale selection, translation workflow, and message formatting.

Agent Protocol

Trigger

Exact user phrases: "i18n", "internationalization", "localization", "l10n", "translation", "multi-language", "locale", "language support", "translation file", "gettext", "message format", "ICU message", "pluralization", "RTL support".

Input Context

Before activating, verify:

  • Initial languages to support and priority order
  • Translation workflow (in-house translators, vendor, Crowdin/Lokalise)
  • Content types needing translation (API messages, emails, push notifications, error messages)
  • Who provides translations (developers, product team, translation service)

Output Artifact

Internationalization architecture design as formatted text.

Response Format

# Locale configuration with fallback chains
# Translation file structure
// Message formatting code
// Locale detection and negotiation

No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.

Completion Criteria

  • Locale selection with BCP 47 tags and fallback chains
  • Message storage with ICU MessageFormat and key naming convention
  • Translation workflow defined (extract → translate → import → validate)
  • API message delivery with Accept-Language parsing and Content-Language header
  • Server-side locale-aware formatting (date, number, currency, timezone)
  • RTL support with dir attribute and bidirectional text handling

Max Response Length

200 lines of configuration and code.

Decision Tree

Which Library?

What tech stack and i18n needs?
  ├── Node.js, need ICU support, flexible
  │   └── i18next (most popular, rich ecosystem)
  ├── React, need ICU, date/number formatting
  │   └── FormatJS / react-intl (ICU native, Intl API integration)
  ├── Python / Django
  │   └── Babel + gettext (PO files, Django integration)
  ├── Python, need ICU
  │   └── Babel with babel-icu extension
  ├── Go
  │   └── go-i18n (ICU-like, YAML/TOML/JSON)
  └── Java / Spring
      └── ResourceBundle + ICU4J (standard Java i18n)

How to Detect Locale?

Where does the user's locale come from?
  ├── Browser sends Accept-Language header
  │   └── Parse q-value, negotiate against supported list
  ├── User has saved preference in profile
  │   └── Use profile locale, ignore Accept-Language
  ├── Domain or subdomain (fr.example.com)
  │   └── Map domain to locale
  ├── GeoIP (approximate location)
  │   └── Use as default only — never override user preference
  └── Authenticated API → JWT contains locale claim
      └── Extract from token, fast-path locale resolution

Workflow

Step 1: Library Selection

LibraryLanguageMessage FormatPluralizationICU NativeFramework
i18nextJS/TSJSON6 formsVia pluginUniversal
FormatJSJS/TSICUBuilt-inYesReact
gettextMultiPO/MO4 formsLimitedPython/PHP
Fluent (Project Fluent)MultiFTLUnlimitedYesMozilla
BabelPythonPO4 formsVia babel-icuDjango

Choose based on: ICU support requirements, framework integration, pluralization rules complexity (some languages have 6+ plural forms), and runtime performance. i18next is most popular for Node.js, FormatJS for React, gettext for Python/Django, Fluent for Mozilla projects.

Step 2: Locale Selection and Detection

Use BCP 47 tags: en-US, vi-VN, zh-CN, de-DE, fr-FR, ja-JP, ar-SA. Define fallback chain per language: es-MX → es-ES → es → en-US. Store supported locales in configuration. Detect user locale from: Accept-Language header (q-value parsing), user profile preference (database), geolocation (CloudFront CloudFront-Viewer-Country header), domain/subdomain (fr.example.com). Never guess locale from IP alone — always respect user preference.

function negotiateLocale(acceptLanguage: string): string {
  const supported = ['en-US', 'vi-VN', 'zh-CN', 'es-MX', 'es-ES', 'fr-FR', 'de-DE', 'ja-JP', 'ar-SA'];
  const fallbacks: Record<string, string> = { 'es-MX': 'es-ES', 'es-ES': 'es', 'es': 'en-US' };
  const parsed = acceptLanguage.split(',')
    .map(s => { const [tag, q = 'q=1'] = s.trim().split(';'); return { tag: tag.trim(), q: parseFloat(q.split('=')[1] || '1') }; })
    .sort((a, b) => b.q - a.q);
  for (const { tag } of parsed) {
    if (supported.includes(tag)) return tag;
    const base = tag.split('-')[0];
    if (supported.includes(base)) return base;
    if (fallbacks[tag]) return fallbacks[tag];
  }
  return 'en-US';
}

Step 3: Message Storage and ICU MessageFormat

Use ICU MessageFormat for all user-facing strings — supports pluralization, gender, select, and number/date formatting. Organize translation files as JSON per locale per domain: locales/en-US/common.json. Key naming convention: {domain}.{context}.{key} — e.g., checkout.error.card_declined, email.welcome.subject. One domain per logical feature area. Shared keys in common.json.

{
  "checkout.error.card_declined": "Your card was declined. {reason, select, insufficient_funds {Insufficient funds.} expired {Card expired.} fraud {Transaction flagged.} other {Please try another method.}}",
  "cart.item_count": "You have {count, plural, =0 {no items} one {# item} other {# items}} in your cart.",
  "invoice.total": "Total: {amount, number, ::currency/USD}",
  "email.greeting": "Hello {name}, your order {orderId} was shipped on {date, date, medium}.",
  "notification.new_follower": "{gender, select, male {He} female {She} other {They}} started following you.",
  "search.ordinal_result": "You are in {n, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} place."
}

ICU MessageFormat patterns reference: {value, date, medium} — date formatting. {value, number, ::currency/USD} — currency. {value, number, ::percent} — percentage. {count, plural, =0 {none} one {# item} other {# items}} — pluralization. {gender, select, male {He} female {She} other {They}} — gender selection. {n, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} — ordinal numbering.

Step 4: Translation Workflow

Extract: CLI scans source code for t()/__() calls and outputs key catalog as JSON with context and file location. Upload: push key catalog to translation platform (Crowdin, Lokalise, POEditor) via REST API. Translate: translators work in platform UI with context strings, screenshots, and descriptions. Review: translation reviewers validate accuracy and consistency. Download: pull translated files as locale JSON. CI validates: every PR checks all keys present, no missing placeholders, ICU syntax valid. Build: bundle translation files with application or lazy-load at runtime.

# Extract keys with i18next-scanner
npx i18next-scanner --config i18next-scanner.config.js

# Push to Crowdin
crowdin upload sources --branch main

# Pull translations (after translators complete)
crowdin download --branch main --skip-untranslated-strings false
// CI validation script
async function validateTranslations(): Promise<boolean> {
  const source = await loadLocale('en-US');
  const locales = ['vi-VN', 'zh-CN', 'es-MX', 'de-DE', 'fr-FR', 'ja-JP', 'ar-SA'];
  let valid = true;
  for (const locale of locales) {
    const target = await loadLocale(locale);
    for (const key of Object.keys(source)) {
      if (!target[key]) { console.error(`Missing key "${key}" in ${locale}`); valid = false; continue; }
      const sourceArgs = extractICUVars(source[key]);
      const targetArgs = extractICUVars(target[key]);
      if (JSON.stringify(sourceArgs.sort()) !== JSON.stringify(targetArgs.sort())) {
        console.error(`Argument mismatch for "${key}" in ${locale}: expected ${sourceArgs}, got ${targetArgs}`);
        valid = false;
      }
    }
  }
  return valid;
}

Step 5: API Message Delivery

Parse Accept-Language header using quality factor (q-value). Negotiate best matching locale from supported set. Set Content-Language response header to the negotiated locale. Translate error messages on server side — never send raw keys to client. For emails/push: use user's stored locale preference. Cache loaded translation files in memory with LRU (max 50 locales, bound memory). For server-rendered pages, negotiate locale per request.

// Express middleware
app.use((req, res, next) => {
  const locale = negotiateLocale(req.headers['accept-language'] || 'en-US');
  req.locale = locale;
  req.t = (key: string, params?: Record<string, unknown>) => i18next.t(key, { lng: locale, ...params });
  res.setHeader('Content-Language', locale);
  next();
});

// Error handler with translated messages
app.use((err: Error, req: Request, res: Response) => {
  const message = req.t('error.generic_server_error', { errorId: req.id });
  res.status(500).json({ error: message, errorId: req.id });
});

Step 6: Locale-Aware Formatting

Use ICU for all formatting: {value, date, medium}, {value, number, ::currency/USD}, {value, number, ::percent}. Timezone: store UTC in DB, convert to user timezone at render time. Number formats differ: 1,234.56 vs 1.234,56. Use Intl.DateTimeFormat, Intl.NumberFormat server-side. Never concatenate translated strings with dynamic values — always use placeholders.

Formaten-USde-DEvi-VNfr-FRja-JP
DateJan 15, 202515.01.202515/01/202515 janv. 20252025/01/15
Time10:30 AM10:3010:3010:3010:30
Number1,234.561.234,561.234,561 234,561,234.56
Currency$1,234.561.234,56 €1.234,56 ₫1 234,56 €¥1,235

Step 7: RTL Support

RTL locales: ar, ar-SA, he, he-IL, fa, fa-IR, ur, ur-PK. Set dir="rtl" attribute in HTML/email response. Handle bidirectional text (BiDi) with Unicode bidi algorithm (UBA). Wrap LTR text in RTL context with Unicode characters: \u202B (RTL Embed), \u202C (Pop Directional Formatting). Use logical CSS properties: margin-inline-start instead of margin-left, padding-inline-end instead of padding-right.

Step 8: Pluralization Rules by Language

LanguagePlural FormsExample
English2 (singular, plural)1 item, 5 items
Russian4 (one, few, many, other)1, 2-4, 5-20, 21
Arabic6 (zero, one, two, few, many, other)0, 1, 2, 3-10, 11-99, 100+
Japanese1 (other)All numbers use same form
Chinese1 (other)All numbers use same form

Step 9: Pseudo-Localization for Testing

// Pseudo-localization: expand strings to find layout issues
function pseudoLocalize(enText: string): string {
  return `[${enText.split('').map(c => {
    const map: Record<string, string> = { 'a': 'α', 'e': 'ε', 'o': 'σ', 'i': 'ι' };
    return map[c.toLowerCase()] || c;
  }).join('')}!!!]`;
}

Lazy Loading Strategy

const translationCache = new Map<string, Record<string, string>>();

async function loadLocale(locale: string, namespace = 'common'): Promise<Record<string, string>> {
  const key = `${locale}:${namespace}`;
  if (translationCache.has(key)) return translationCache.get(key)!;
  const response = await fetch(`/locales/${locale}/${namespace}.json`);
  const data = await response.json();
  translationCache.set(key, data);
  return data;
}

ICU MessageFormat Deep Dive

// Advanced ICU MessageFormat patterns

// Complex select with nested plurals
const complexPattern = `{items, plural,
  =0 {No items in your cart.}
  one {You have # item ({subtotal, number, ::currency/USD})}
  other {
    You have {items_count_formatted} items.
    {subtotal, number, ::currency/USD} subtotal.
    {items, plural, one {# includes a pre-order item.} other {# include pre-order items.}}
  }
}`;

// Ordinal + duration formatting
const orderStatus = `Your order is {position, selectordinal,
  one {#st}
  two {#nd}
  few {#rd}
  other {#th}
} in the queue. Estimated time: {eta, duration}`;

// Rich text formatting (i18next)
// richText_key: "Hello <bold>{{name}}</bold>, please <link>click here</link>"
// Usage:
// t('richText_key', { name: 'John', bold: (text) => `<strong>${text}</strong>`, link: (text) => `<a href="/">${text}</a>` })

Translation Platform Integration

CI/CD Translation Pipeline

# .github/workflows/translations.yml
name: Translation Sync
on:
  push:
    branches: [main]
    paths: ['locales/en-US/**']

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Extract translation keys
        run: npx i18next-scanner --config i18next-scanner.config.js
      - name: Push source to Crowdin
        run: crowdin upload sources --branch main
      - name: Download translations
        run: crowdin download --branch main --skip-untranslated-strings false
      - name: Validate translations
        run: npx ts-node scripts/validate-translations.ts
      - name: Check coverage threshold
        run: npx ts-node scripts/check-coverage.ts --threshold 80
// i18next-scanner config
// i18next-scanner.config.js
module.exports = {
  input: ['src/**/*.{ts,tsx}', '!src/**/*.test.{ts,tsx}'],
  output: './locales',
  options: {
    debug: true,
    removeUnusedKeys: true,
    sort: true,
    func: {
      list: ['t', 'i18next.t', 'i18n.t'],
      extensions: ['.ts', '.tsx'],
    },
    lngs: ['en-US', 'vi-VN', 'zh-CN', 'es-MX', 'de-DE', 'fr-FR', 'ja-JP', 'ar-SA'],
    defaultLng: 'en-US',
    ns: ['common', 'checkout', 'email', 'error', 'notification'],
    defaultNs: 'common',
    resource: {
      loadPath: 'locales/{{lng}}/{{ns}}.json',
      savePath: 'locales/{{lng}}/{{ns}}.json',
    },
    context: true, // Enable context-based keys (e.g., button.save, button.cancel)
    contextFallback: true,
  },
};

Locale-Specific Formatting Rules

// Date/time formatting per locale
const dateFormats: Record<string, Intl.DateTimeFormatOptions> = {
  'en-US': { month: 'short', day: 'numeric', year: 'numeric' },           // "Jan 15, 2025"
  'de-DE': { day: '2-digit', month: '2-digit', year: 'numeric' },         // "15.01.2025"
  'vi-VN': { day: '2-digit', month: '2-digit', year: 'numeric' },         // "15/01/2025"
  'ja-JP': { year: 'numeric', month: '2-digit', day: '2-digit' },         // "2025/01/15"
  'ar-SA': { day: 'numeric', month: 'long', year: 'numeric', calendar: 'islamic' }, // "15 محرم 1446"
};

// Number formatting differences
// Number grouping: 1,234,567.89 (en) vs 1.234.567,89 (de) vs 12,34,567.89 (hi)
// Use Intl.NumberFormat with locale — never hardcode separators
function formatNumber(value: number, locale: string, options?: Intl.NumberFormatOptions): string {
  return new Intl.NumberFormat(locale, options).format(value);
}

// Currency formatting: handle placement and symbol variation
// $1,234.56 (en-US) vs 1.234,56 € (de-DE) vs 1,234.56 USD (en-CA)
function formatCurrency(value: number, currency: string, locale: string): string {
  return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value);
}

Server-Side Caching Strategy

// Translation cache with fallback chains and TTL
class TranslationCache {
  private cache = new Map<string, Record<string, string>>();
  private ttlMs: number;
  private maxSize: number;

  constructor(ttlMs = 3600000, maxSize = 50) {
    this.ttlMs = ttlMs;
    this.maxSize = maxSize;
  }

  async getTranslation(locale: string, namespace: string): Promise<Record<string, string>> {
    const key = `${locale}:${namespace}`;
    const cached = this.cache.get(key);

    if (cached) return cached;

    // Load from file system or CDN with fallback chain
    const translations = await this.loadWithFallback(locale, namespace);

    // Evict oldest entry if at capacity
    if (this.cache.size >= this.maxSize) {
      const oldest = this.cache.keys().next().value;
      this.cache.delete(oldest);
    }

    this.cache.set(key, translations);
    setTimeout(() => this.cache.delete(key), this.ttlMs);
    return translations;
  }

  private async loadWithFallback(locale: string, namespace: string): Promise<Record<string, string>> {
    const fallbacks = [locale, locale.split('-')[0], 'en-US'];
    for (const lang of fallbacks) {
      try {
        return await loadTranslationFile(lang, namespace);
      } catch {
        continue;
      }
    }
    return {};
  }
}

Configuration Reference

i18n:
  defaultLocale: en-US
  supportedLocales: [en-US, vi-VN, zh-CN, es-MX, es-ES, de-DE, fr-FR, ja-JP, ar-SA]
  rtlLocales: [ar-SA, he-IL, fa-IR, ur-PK]
  fallbackStrategy: exact -> parent -> default -> key
  namespaces: [common, checkout, email, error, notification]
  cache:
    type: lru
    maxSize: 50
    ttlMs: 3600000
  lazyLoad: true
  pseudoLocalization: false
  ci:
    validatePlaceholders: true
    validateSyntax: true
    requireAllKeys: true

Production Considerations

ConcernPractice
Translation file sizeSplit by namespace. Namespace per feature. Lazy-load on first use
Missing translationsFallback chain: exact → parent → default → key name returned
PerformanceCache translations in-memory. LRU with TTL
CDN for translation filesServe locale JSON from CDN with versioned URLs
Translation coverageCI fails if coverage < 80% of source keys
Context for translatorsEvery key has description, max length hint, and screenshot
Dynamic content (user names, dates)Always use ICU placeholders — never concatenate translated text with values
RTL layoutFlip layout mirrors, icons, and alignment in addition to text direction
Number of supported localesEach locale adds maintenance cost. Only add when business need exists
Translation memory reuseReuse identical translations across keys to save translator cost

Security

RiskMitigation
Injection via translationICU MessageFormat supports code execution in some parsers — always validate/sanitize
Missing placeholdersCI validates argument matching between source and target locales
RTL override injectionSanitize user-generated content in RTL contexts (Unicode bidi overrides)
Excessive locale loadingRate-limit locale file requests, cache aggressively
Translator access to source codeUse translation platform with proper RBAC — translators should not need repo access
Exposed translation keys in APIReturn translated messages, never raw keys, in API responses

Anti-Patterns

Anti-PatternWhy It's BadFix
English-only keys as fallback (e.g., checkout.error.card_declined returns "checkout.error.card_declined")Confusing for usersFallback to default locale (en-US) translation, not the key
String concatenationBreaks in RTL, wrong word orderUse placeholders: Hello {name} not "Hello " + name
Large monolithic translation filesSlow to load, hard to maintainSplit by namespace per feature
No ICU for pluralsWrong grammar in many languagesUse ICU plural syntax
Client-side only i18nSEO fails, server errors not translatedTranslate on server for SSR and API errors
Inline strings mixed with codeImpossible to extract for translatorsAll strings in translation files, none in code
Machine-only translations without reviewLow quality, cultural insensitivityAlways have human review machine translations before release
Over-translation of technical termsBrand names, product names, and commands should stay untranslatedUse ICU select or =0 syntax for untranslated terms

Rules

  • Locale = BCP 47 tag. Never language-only (en not english)
  • Keys are dot-separated and namespaced
  • ICU MessageFormat for all user-facing strings
  • Fallback chain: exact locale → parent locale → default locale → key itself
  • Translation files are version-controlled and CI-validated
  • Every string has a description for translators
  • No concatenation of translated strings — use placeholders
  • Store timestamps in UTC, format at render time
  • RTL languages require dir="rtl" attribute and BiDi handling
  • Never translate error messages at client — always translate server-side
  • Extract translation strings as CI step, never manually maintain key lists

References

  • references/i18n-architecture.md — Internationalization Architecture
  • references/i18n-frontend.md — Frontend Internationalization
  • references/i18n-libraries.md — i18n Libraries
  • references/i18n-performance.md — i18n Performance
  • references/i18n-testing.md — i18n Testing
  • references/l10n-patterns.md — L10n Patterns
  • references/rtl-i18n.md — RTL and Complex Scripts Reference
  • references/translation-workflow.md — Translation Workflow Reference

Handoff

frontend-universal/animation for RTL UI considerations and animation direction

Implementation Patterns

Observer Pattern for Event Handling

` interface EventObserver { onEvent(event: T): Promise; }

class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `

Configuration-Driven Approach

config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1

Production Considerations

Deployment Checklist

  • Configuration validated against schema before startup
  • Health check endpoints registered and monitored
  • Graceful shutdown with draining period (30s timeout)
  • Resource limits configured (CPU, memory, file descriptors)
  • Log level set appropriate for environment
  • Metrics endpoint secured and exposed
  • Rate limiting configured per-tier
  • TLS certificates valid and auto-renewing
  • Database migrations run as separate deployment step
  • Feature flags ready for gradual rollout

Monitoring and Alerting

MetricThresholdSeverityAction
Error rate> 1% over 5minCriticalPage on-call
p99 latency> 2s over 5minWarningInvestigate
Throughput drop> 50% over 1minCriticalCheck upstream
Queue depth> 1000 over 1minWarningScale consumers
Disk usage> 85%WarningClean or expand
Memory usage> 90% heapCriticalRestart or scale

Anti-Patterns

Anti-PatternSymptomRoot CauseSolution
Premature optimizationComplex code for no measured benefitGuessing instead of profilingMeasure first, optimize based on data
Copy-paste reuseDuplicate code across codebaseLack of abstractionExtract shared logic into libraries
Gold-platingFeatures with no current requirementOver-engineeringYAGNI — build what's needed now
Magical thinkingAssumptions without validationSkipping error handlingHandle all failure modes explicitly

Performance Optimization

Caching Strategy

Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).

Resource Pooling

  • Database connections: Pool of reusable connections (HikariCP, pgBouncer)
  • HTTP connections: Keep-alive + connection pooling for external calls
  • Thread pool: Bounded thread pools for async task execution

Profiling Methodology

  1. Establish baseline with production traffic profile
  2. Profile CPU with sampling profiler (pprof, perf, async-profiler)
  3. Profile memory with heap dumps and allocation tracking
  4. Profile I/O with strace/perf trace for syscall analysis
  5. Profile latency with distributed tracing (OpenTelemetry)
  6. Identify bottleneck, formulate hypothesis, implement fix
  7. Re-profile to verify improvement, repeat

Security Considerations

Threat Modeling (STRIDE)

  • Spoofing: Identity validation, authentication
  • Tampering: Integrity checks, digital signatures
  • Repudiation: Audit logs, non-repudiation
  • Information disclosure: Encryption, access control
  • Denial of service: Rate limiting, resource quotas
  • Elevation of privilege: Principle of least privilege

Supply Chain Security

  • Dependency scanning: Snyk, Dependabot, Trivy
  • SBOM generation: CycloneDX or SPDX format
  • Signed commits: GPG or SSH commit signing
  • Artifact verification: Checksum validation, signature verification

Secrets Management

  • Secrets never in code — always in secrets manager (Vault, AWS Secrets Manager)
  • Rotation policy: Rotate database credentials every 90 days
  • Access audit: Log every secrets access, alert on anomalies
  • Encryption at rest and in transit for all secrets
  • Principle of least privilege: each service gets only its own secrets

Rules

  • Default-deny security posture — allow only explicitly required access.
  • All inputs validated, all outputs encoded, all errors handled.
  • Defend in depth — multiple layers of security controls.
  • Fail securely — errors default to safe behavior.
  • Log security-relevant events for audit and investigation.
  • Keep dependencies updated — automate vulnerability scanning.
  • Design for observability from day one, not as an afterthought.
  • Document all architectural decisions with rationale.
  • Review code for security, performance, and correctness before merging.
Skills guardadas